0%

前言

最近想在公司远程连接家里的电脑,但苦于现在的家庭网络没有独立ip,导致无法通过ip直连。

作为一个程序员,这怎么能难倒我们,通过网上搜索找到以下几个解决方法
1、打电话给运营商让给外网ip

这个方法随着ip4资源越来越少,估计越来越不现实

2、通过网络穿透,类似花生壳或ngrok这些服务,把局域网服务通过服务器暴露给外网调用。

此方案靠谱,本人最后选择了自建ngrok服务搭建网络穿透。

NGROK简介

官方的解释为

ngrok 是一个反向代理,它创建从公共端点到本地运行的 Web 服务的安全隧道。 ngrok 捕获并分析隧道上的所有流量,以供以后检查和重放。

简单来说,你个人本地web服务,通过与ngrok的服务建立隧道连接,然后别人就可以通过访问ngrok服务来访问你的本地服务了。

ngrok的git地址: https://github.com/inconshreveable/ngrok

ngrok的官网: https://ngrok.com/

商业版本的ngrok已经到2.0了,但是源码并不开源,开源的只是1.0。官方也不建议使用1.0,因为客户端和服务器都存在严重的可靠性问题,包括内存和文件描述符泄漏以及崩溃。

使用ngrok有两种方法

  • 直接使用官方ngrok服务,服务端是现成的,分为国外和国内,我们只需安装客户端即可,操作手册参见ngrok官网
  • 使用ngrok1.0git源码 ,自行编译和搭建ngrok服务端和客户端。

本人最后采用了编译ngrok源码的方式,搭建服务端和客户端,并最后实现windows远程连接。

NGROK的安装

网上的相关资料比较多,这里就不展开说了,本人参考了以下两篇。
云服务器搭建自己的ngrok服务-实现内网穿透
https://cloud.tencent.com/developer/article/1100382

个人最后总的命令如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# 设置域名
NGROK_DOMAIN=ngrok.oxxx.com

# 使用openssl证书域名跳过,本人最后采用阿里云免费证书的方式 具体可以参考 https://blog.csdn.net/qq_42063179/article/details/120620693
# 证书的替换方式为:
xxx.pem > assets/server/tls/snakeoil.crt
xxx.pem > assets/client/tls/ngrokroot.cr
xxx.key > assets/server/tls/snakeoil.key

# openssl genrsa -out rootCA.key 2048
# openssl req -x509 -new -nodes -key rootCA.key -subj "/CN=$NGROK_DOMAIN" -days 5000 -out rootCA.pem
# openssl genrsa -out device.key 2048
# openssl req -new -key device.key -subj "/CN=$NGROK_DOMAIN" -out device.csr
# openssl x509 -req -in device.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out device.crt -days 5000

# cat rootCA.pem > assets/client/tls/ngrokroot.crt
# cat device.crt > assets/server/tls/snakeoil.crt
# cat device.key > assets/server/tls/snakeoil.key

# 关闭模块
go env -w GO111MODULE=off

# 开启模块,使用代理
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct


#编译服务端 linux 64位 服务器
GOOS=linux GOARCH=amd64 make release-server

# 编译客户端 64位windows客户端:
GOOS=windows GOARCH=amd64 make release-client
#编译客户端 mac 64 位 客户端
GOOS=darwin GOARCH=amd64 make release-client
#编译客户端 linux 64 位客户端
GOOS=linux GOARCH=amd64 make release-client

# 启动服务
nohup /opt/soft/ngrok/bin/ngrokd -tlsKey=/opt/soft/ngrok/assets/server/tls/snakeoil.key -tlsCrt=/opt/soft/ngrok/assets/server/tls/snakeoil.crt -domain="ngrok.xxx.com" -httpAddr=":9101" -httpsAddr=":9104" -tunnelAddr=":9103" > /opt/soft/ngrok/logs/ngrok.log 2>&1 &
tail -f /opt/soft/ngrok/logs/ngrok.log


# 进入客户端ngrok目录
cd ngrok
# 新增客户端配置文件 ngrok.cfg
vi ngrok.cfg

#ngrok.cfg 文件内容
server_addr: "ngrok.xxx.com:9103"
trust_host_root_certs: false
tunnels: #可定义多个域名
test1:
subdomain: "test1" #定义服务器分配域名前缀
proto:
http: 8089 #映射端口,不加ip默认本机

mstsc: # windows 远程桌面
remote_port: 4499
proto:
tcp: 3389 #映射端口,不加ip默认本机

# 启动客户端
./ngrok -config=ngrok.cfg -log=ngrok.log start test2

# 访问远程左面地址
ngrok.xxx.com:4499

安装避坑指南

1、使用go编译ngrok卡住
因为go需要依赖第三方工程,由于伟大的长城在,网络不通畅,可以关闭模块或配置代理,都有效。

1
2
3
4
5
6
# 关闭模块
go env -w GO111MODULE=off

# 开启模块,使用代理
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct

2、openssl证书认证错误

1
ngrok certificate relies on legacy Common Name field, use SANs instead

这是因为common name field 模式计划删除了20年都没删除,go语言在1.15之后之后不再支持。

解决方式吗,要么降低go语言版本(不推荐),要么使用推荐的SAN替换(本人没跑通),要么直接使用现成的证书(本人使用了阿里云的免费证书)

1、阿里云免费证书申请参见 2022阿里云免费SSL证书申请全过程(图文详解)

2、证书下载方式为其他,下载后替换如下
xxx.pem > assets/server/tls/snakeoil.crt
xxx.pem > assets/client/tls/ngrokroot.cr
xxx.key > assets/server/tls/snakeoil.key

WINDOWS 远程桌面开启&ngrok客户端配置

如何开启远程桌面,这个网上较多,可以参考如何使用远程桌面
如果处于安全考虑,可以修改远程桌面端口,可参考 修改远程桌面端口

ngrok 客户端配置,可参考 使用ngrok实现远程桌面连接

乐观锁

乐观锁不是数据库自带的,需要我们自己去实现。乐观锁是指操作数据库时(更新操作),想法很乐观,认为这次的操作不会导致冲突,在操作数据时,并不进行任何其他的特殊处理(也就是不加锁),而在进行更新后,再去判断是否有冲突了。

通常实现是这样的:在表中的数据进行操作时(更新),先给数据表加一个版本(version)字段,每操作一次,将那条记录的版本号加1。也就是先查询出那条记录,获取出version字段,如果要对那条记录进行操作(更新),则先判断此刻version的值是否与刚刚查询出来时的version的值相等,如果相等,则说明这段期间,没有其他程序对其进行操作,则可以执行更新,将version字段的值加1;如果更新时发现此刻的version值与刚刚获取出来的version的值不相等,则说明这段期间已经有其他程序对其进行操作了,则不进行更新操作。

举例:

下单操作包括3步骤:

  1. 查询出商品信息

    1
    select (status,status,version) from t_goods where id=#{id}
  2. 根据商品信息生成订单

  3. 修改商品status为2

    1
    2
    3
    update t_goods
    set status=2,version=version+1
    where id=#{id} and version=#{version};

    除了自己手动实现乐观锁之外,现在网上许多框架已经封装好了乐观锁的实现,如hibernate,需要时,可能自行搜索”hiberate 乐观锁”试试看。

悲观锁

与乐观锁相对应的就是悲观锁了。悲观锁就是在操作数据时,认为此操作会出现数据冲突,所以在进行每次操作时都要通过获取锁才能进行对相同数据的操作,这点跟java中的synchronized很相似,所以悲观锁需要耗费较多的时间。另外与乐观锁相对应的,悲观锁是由数据库自己实现了的,要用的时候,我们直接调用数据库的相关语句就可以了。

说到这里,由悲观锁涉及到的另外两个锁概念就出来了,它们就是共享锁与排它锁。共享锁和排它锁是悲观锁的不同的实现,它俩都属于悲观锁的范畴。

共享锁(S锁)

共享锁(S锁):共享 (S) 指的就是对于多个不同的事务,对同一个资源共享同一个锁. 用于不更改或不更新数据的操作(只读操作),如 SELECT 语句。

如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。

刚刚说了,对于悲观锁,一般数据库已经实现了,共享锁也属于悲观锁的一种,那么共享锁在mysql中是通过什么命令来调用呢。通过查询资料,了解到通过在执行语句后面加上lock in share mode就代表对某些资源加上共享锁了。
比如,我这里通过mysql打开两个查询编辑器,在其中开启一个事务,并不执行commit语句
city表DDL如下:

1
2
3
4
5
6
7
8
9
CREATE TABLE `city` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`state` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8;

begin;
SELECT * from city where id = "1" lock in share mode;

然后在另一个查询窗口中,对id为1的数据进行更新

1
2
3
4
update city set name="666" where id ="1";
此时,操作界面进入了卡顿状态,过几秒后,也提示错误信息
[SQL]update city set name="666" where id ="1";
[Err] 1205 - Lock wait timeout exceeded; try restarting transaction

那么证明,对于id=1的记录加锁成功了,在上一条记录还没有commit之前,这条id=1的记录被锁住了,只有在上一个事务释放掉锁后才能进行操作,或用共享锁才能对此数据进行操作。

排他锁(X锁)

排他锁(X锁):排它锁与共享锁相对应,就是指对于多个不同的事务,对同一个资源只能有一把锁。用于数据修改操作,例如 INSERT、UPDATE 或 DELETE。确保不会同时同一资源进行多重更新。

如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。

我们在操作数据库的时候,可能会由于并发问题而引起的数据的不一致性(数据冲突)

行锁

行锁,由字面意思理解,就是给某一行加上锁,也就是一条记录加上锁。

比如之前演示的共享锁语句

1
SELECT * from city where id = "1" lock in share mode;

由于对于city表中,id字段为主键,就也相当于索引。执行加锁时,会将id这个索引为1的记录加上锁,那么这个锁就是行锁。

表锁

表锁,和行锁相对应,给这个表加上锁。

建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有:使用句柄、直接指针。

句柄
如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。

直接指针
如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。

这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。

引言

二分查找的大名,如雷贯耳,它可以在有序的一个数组中实现实现$O(logn)$的复杂度查找。

然而,业界还流传着一句话

十个二分九个错

这句调侃的话可谓精准的描述了二分查找的特性,二分查找从实现思路上是非常简单的,利用有序特性,对半查找,不断缩小范围,直到找到结果。然而在细节边界问题上,二分查找可以说是难得过分,如:

  • 退出循环的标准,是< 还是 <= ?
  • mid的计算标准,是(left+right)/2还是 (left+right+1)/2 ?
  • 折半的移动标准,是 =mid 还是 =mid-1 ?
  • 返回值的选定,怎么有写是left,有写是right,有写还要额外判断?

因为这些边界问题,导致二分查找以为懂了的你,写出的代码总是不行,本文为个人为了彻底弄懂二分查找,一劳永逸的从根本上理解的沉淀。

二分查找的核心思想

二分查找的核心思想就是对半查找,如下图所示,有一个[1,2,3,4,5,6,7] 数组,要判断2是否存在于这个数据中。

二分查找的方法为:

  1. 数组[1,2,3,4,5,6,7]的中间值为 (1+7)/2=4,找第四个数字,该值为4,由于4!=2,因此需要继续判断,又由于4>2,因此得出目标值在左边这个区间,即[1,2,3]这里面
  2. 数组[1,2,3]的中间值为 (1+3)/2=2,找第二个数字,该值为2,是期望要找的目标,返回ture。

核心思想非常简单,非常简介。

二分查找的分类


二分查找的类型可以分为

  • 寻找target值的下标
  • 寻找左侧边界
  • 寻找右侧边界
  • 寻找小于target值的最大值
  • 寻找>target值的最小值

其中
寻找左侧边界= 寻找>=target值的最小值
寻找右侧边界= 寻找<=target值的最大值

不同类型的二分代码

先直接给代码,之后再通过分析代码直接的异常点来确认为什么代码要这么写。

最简单的二分,寻找target值的下标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 /**
* 最简单的二分,不会出错,判断目标值是否在array中
* */
public int binarySearch(int[] array,int target){
int l=0;
int r=array.length-1;
while (l<=r){
int mid=(l+r)>>1;
if(array[mid]==target){
return mid;
}else if(array[mid]<target){
l=mid+1;
}else{
r=mid-1;
}
}
return -1;
}

寻找左侧边界

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 寻找左侧边界
* */
public int binarySearchLeft(int[] array,int target){
int l=0;
int r=array.length-1;
while (l<r){
int mid=(l+r)>>1;
if(array[mid]>=target){
r=mid;
}else{
l=mid+1;
}
}
return array[l]==target?l:-1;
}

寻找右侧边界

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 寻找右侧边界
* */
public int binarySearchRight(int[] array,int target){
int l=0;
int r=array.length-1;
while (l<r){
int mid=(l+r+1)>>1;
if(array[mid]<=target){
l=mid;
}else{
r=mid-1;
}
}
return array[r]==target?r:-1;
}

寻找小于target的最大值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 寻找小于target的最大值
* */
public int binarySearchLtTarget(int[] array,int target){
int l=0;
int r=array.length-1;
while (l<r){
int mid=(l+r+1)>>1;
if(array[mid]>=target){
r=mid-1;
}else{
l=mid;
}
}
return array[r]<target?r:-1;
}

寻找大于target的最小值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 寻找大于target的最小值
* */
public int binarySearchGtTarget(int[] array,int target){
int l=0;
int r=array.length-1;
while (l<r){
int mid=(l+r)>>1;
if(array[mid]<=target){
l=mid+1;
}else{
r=mid;
}
}
return array[l]>target?l:-1;
}
  1. 区间判断采用闭区间

4步掌握二分查找写法

要想写出不出错的二分查找,必须得有区间思维,带着去见思维去写代码。个人总结了4步操作

1.二分查找框架模板伪代码

掌握二分查找框架模板,之后再一步一步实现细节,一个通用的模板如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 二分查找通用伪代码
* */
public int binarySearch(int[] array,int target){
int l=0;
int r=array.length-1;
while (loopCondition(l,r)){
int mid=calcMid(l,r);
if(check(array[mid],target)){
l=changeLeft(mid);
}else{
r=changeRight(mid);
}
}
return rs;
}

其中,exitLoopCondition,calcMid,check,changeLeft,changeRight,rs等都为伪代码,需要后续去实现。

2. 确认进入循环条件

确认进入循环的条件,即确认上文伪代码中的exitLoopCondition片段。

确认循环进入条件,首先确认我们的左右查询区间。

  • 如果是$[l,r]$的闭区间,那么判断条件应该为 l<=r
  • 如果是$[l,r)$的左闭又开区间,那么判断条件应该为 l<r

至于$(l,r]$左开右闭及$(l,r)$左开右开,由于太过特例独行,本篇就不进行考虑,当人个人可以进行推导。

所以,为什么$[l,r]$的闭区间,判断条件应该为$l<=r?$,而$[l,r)$的左闭又开区间,判断条件应该为 l<r?

对于二分查找,如果区间内不为空且存在值,那么就应该进入循环继续判断。
那么进入循环的条件判断就转化为,l,r满足什么条件,区间内存在值且不为空?
这个问题只要动用简单的数学知识,就能知道。

  • 对于闭区间$[l,r]$,当边界区间内有值的条件为$l<=r$,因此当$l<=r$时,都应该进入循环。
  • 而对于左闭右开区间$[l,r)$,边界的有值条件为$l<r$

是不是感觉和之前的话一模一样,但是视角不一样了。

3. mid值的计算

mid值的计算写法有很多种,本文列举几种

1
2
3
4
5
6
7
8
9
10
# 左边界
int mid=(l+r)/2;
int mid=(l+r)>>1;
int mid=l+(r-l)/2;
int mid=l+((r-l)>>1);
# 右边界
int mid=(l+r+1)/2;
int mid=(l+r+1)>>1;
int mid=l+(r-l+1)/2;
int mid=l+((r-l+1)>>1);

tips

  1. mid值可能溢出,因此如果可以,将int升级为long类型
  2. 位运算符比除号性能更好
  3. 求右边界时,需要+1
  4. 使用位运算符,注意位运算的计算优先级

4. 区间结果判断&区间变更

本段主要确认的代码为

1
2
3
4
5
if(check(array[mid],target)){
l=changeLeft(mid);
}else{
r=changeRight(mid);
}

从代码上来说,则是确认 l=mid,r=mid-1还是l=mid-1,r=mid

还是区间的概念,只要简单分析就明白了。

对于左开右闭区间$[l,r)$,二分后的区间应该为$[l,mid)$和$[mid,r)$

4.1 确认mid的归属,归属左半边还是右半边

常见问题

边界分类

文章参考

二叉树的遍历主要有3中,前序、中序和后序。

很多人经常只记住了名字,但是记不住前序、中序和后序到底是如何遍历的。

这里我们只要记住,前序,中序和后序指的是根节点的位置即可,即(根)前序,(根)中序,(根)后序,意思就是根节点在根节点、左节点,右节点这三个节点时遍历的顺序。
(根前序)根左右

(根中序)左根右

(根后序)左右根

这样子就非常形象了。

下面,根据实际的一个二叉树举例说明

前序遍历结果为:FDBACEGIHJ

中序遍历结果为:ABCDEFGIHJ

后序遍历结果为:ACBEDHJIGF

转载

本文转自 CAP 定理的含义

前言

分布式系统(distributed system)正变得越来越重要,大型网站几乎都是分布式的。

分布式系统的最大难点,就是各个节点的状态如何同步。CAP 定理是这方面的基本定理,也是理解分布式系统的起点。

本文介绍该定理。它其实很好懂,而且是显而易见的。下面的内容主要参考了 Michael Whittaker 的文章

一、分布式系统的三个指标


1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标。

1
2
3
Consistency
Availability
Partition tolerance

它们的第一个字母分别是 C、A、P。

Eric Brewer 说,这三个指标不可能同时做到。这个结论就叫做 CAP 定理。

二、Partition tolerance

先看 Partition tolerance,中文叫做”分区容错”。

大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。

上图中,G1 和 G2 是两台跨区的服务器。G1 向 G2 发送一条消息,G2 可能无法收到。系统设计的时候,必须考虑到这种情况。

一般来说,分区容错无法避免,因此可以认为 CAP 的 P 总是成立。CAP 定理告诉我们,剩下的 C 和 A 无法同时做到。

三、Consistency

Consistency 中文叫做”一致性”。意思是,写操作之后的读操作,必须返回该值。举例来说,某条记录是 v0,用户向 G1 发起一个写操作,将其改为 v1。

接下来,用户的读操作就会得到 v1。这就叫一致性。

问题是,用户有可能向 G2 发起读操作,由于 G2 的值没有发生变化,因此返回的是 v0。G1 和 G2 读操作的结果不一致,这就不满足一致性了。

为了让 G2 也能变为 v1,就要在 G1 写操作的时候,让 G1 向 G2 发送一条消息,要求 G2 也改成 v1。

这样的话,用户向 G2 发起读操作,也能得到 v1。

四、Availability

Availability 中文叫做”可用性”,意思是只要收到用户的请求,服务器就必须给出回应。

用户可以选择向 G1 或 G2 发起读操作。不管是哪台服务器,只要收到请求,就必须告诉用户,到底是 v0 还是 v1,否则就不满足可用性。

五、Consistency 和 Availability 的矛盾

一致性和可用性,为什么不可能同时成立?答案很简单,因为可能通信失败(即出现分区容错)。

如果保证 G2 的一致性,那么 G1 必须在写操作时,锁定 G2 的读操作和写操作。只有数据同步后,才能重新开放读写。锁定期间,G2 不能读写,没有可用性不。

如果保证 G2 的可用性,那么势必不能锁定 G2,所以一致性不成立。

综上所述,G2 无法同时做到一致性和可用性。系统设计时只能选择一个目标。如果追求一致性,那么无法保证所有节点的可用性;如果追求所有节点的可用性,那就没法做到一致性。

在什么场合,可用性高于一致性?

举例来说,发布一张网页到 CDN,多个服务器有这张网页的副本。后来发现一个错误,需要更新网页,这时只能每个服务器都更新一遍。

一般来说,网页的更新不是特别强调一致性。短时期内,一些用户拿到老版本,另一些用户拿到新版本,问题不会特别大。当然,所有人最终都会看到新版本。所以,这个场合就是可用性高于一致性。

Base64 是网络中存储和传输的二进制数据的普遍用法。Base64 一个字节只能表示 64 种情况,且编码格式每个字节的前两位都只能是 0,使用剩下的 6 位表示内容。

看到这里相信大家也能够意识到,这种编码格式无法充分利用存储资源,效能较低。那为什么还会成为网络中的普遍用法呢?

其实 Base64 最早是应用在邮件传输协议中的。当时邮件传输协议只支持 ASCII 字符传递,使用 ASCII 码来表示所有的英文字符和数字还有一些符号。这里有一个问题,如果邮件中只传输英文数字等,那么 ASCII 可以直接支持。但是如果要在文件中传输图片、视频等资源的话,这些资源转成 ASCII 的时候会出现非英文数字的情况。而且邮件中还存在很多控制字符,这些控制字符又会成为不可见字符。非英文字符和控制字符在传输过程中很容易产生错误,影响邮件的正确传输。为此才有了诞生了一个新的编码规则,把二进制以 3 个字节为一组,再把每组的 3 个字节(24 位)转换成 4 个 6 位,每 6 位根据查表对应一个 ASCII 符号,这就是 Base64。

Base64 将 8 位为一个单元的字节数据,拆分为 6 位为一个单元的二进制片段。每一个 6 位单元对应 Base64 索引表中的一个字符。简单举个例子,下图中 M 的 ASCII 码是 77 , 而转换为二进制后前六位二进制对应值为 19,为 Base64 字典中的 T。

当然这里也会有一个问题,如果要编码的二进制数据不是 3 的倍数,那就会剩下一至二个字节。为此 Base64 使用 000000 字节值在末尾补足,使其字节数能够被 3 整除,补位用 = 表示,= 的个数可表示补了多少字节,并在解码时自动去除。总体来看相比编码前,Base64 编码后的字符增加了约 33%。


IDEA整合LeetCode的插件,有了这个插件,可以在IDEA本地编辑代码并且运行提交,还能关联自己的账号,简直实用之极。看网上介绍的都不太详细,我来写个清楚点的。插件如图:

一:下载插件

点击intelij Idea->Preferences->Plugins:

搜索leetcode下载就行了。如果你的搜不到,可以尝试重新打开Setting重新搜,还没有的话,可以去官网插件库下载,然后导入就可以了。链接:https://plugins.jetbrains.com/plugin/12132-leetcode-editor

二:配置

安装完成之后,点击IdealiJ idea->Preference->Tools->leetcode plugin,

也可以点击右下角的leetcode图标

配置界面如图:

注意:上图中TempFilePath对应的文件夹一定要是你此项目模块源码的位置 。我的新建一个项目的意思是,像我那样重新建一个名为“LeetCode”的项目,然后选择其src目录,评论区红色无效文件可能就是这个原因。

关于下面几个参数的定义,官方给的是:

  • Custom code template: 开启使用自定义模板,否则使用默认生成格式

  • CodeFileName: 生成文件的名称,默认为题目标题

  • CodeTemplate: 生成题目代码的内容,默认为题目描述和题目代码

  • TemplateConstant: 模板常用变量

  • ${question.title}:题目标题,例如:两数之和

  • ${question.titleSlug}:题目标记,例如:two-sum

  • ${question.frontendQuestionId}:题目编号,例如:1

  • ${question.content}:题目描述内容

  • ${question.code}:题目代码部分

  • $!velocityTool.camelCaseName(str):一个函数,用来将字符串转化为驼峰样式

CodeFileName这个里面填的就是以后自动生成类的类名,使用我的这个配置刚好可以

1
P$!{question.frontendQuestionId}$!velocityTool.camelCaseName(${question.titleSlug})

CodeTemplate就是自动生成的代码格式,对于有强迫症的人来说,这个自动生成的格式就非常重要了,不然看着心里就烦。其中main()方法是用来debug的。我的配置(如果复制过去格式不对,请手动改成我这样的,空行也不要删):

1
2
3
4
5
6
7
8
9
10
package leetcode.editor.cn;
${question.content}
public class $!velocityTool.camelCaseName(${question.titleSlug}){undefined
public
static
void main(String[] args) {undefined
Solution solution = new $!velocityTool.camelCaseName(${question.titleSlug})().new Solution();
}
${question.code}
}

就这样自动生成的代码是这样的,个人觉得还可以:

注意:

在生成的自定义代码中包含两行关键信息:

  • leetcode submit region begin(Prohibit modification and deletion):提交到leetcode进行验证的代码开始标记

  • leetcode submit region end(Prohibit modification and deletion):提交到leetcode进行验证的代码结束标记

这两行标记标示了提交到leetcode服务器进行验证的代码范围,在此范围内只允许有出现与题目解答相关的内容,出现其他内容可能导致leetcode验证不通过。

除了此范围内,其他区域是可以任意填写的,内容不会提交到leetcode,可以增加一些可以本地调试的内容,例如:import java.util.Arrays;

所以,这两行内容是不能被删除和修改的,否则将识别不到提交的内容。

补充:

如图中的文档注释中的类,没有快捷键可以一次性取消,如果一行一行删又太费事 ,我们可以用这个方法。

光标放在这里,按下Alt+鼠标左键,就可以对多行进行删除,简单方便。

三:使用

点击左下角的按钮,然后点击上面的小地球进行联网登录,登陆成功就是图中的画面了。双击题目,就会自动创建类

写完代码,右键

如图,就可以运行测试和提交了,在下面的Event Log可以查看运行情况

spark 的启动流程?

shell端的启动流程

以一个常规的wordcount 程序spark启动命令为例,spark在shell端的启动流程如下

  1. 首先,用户在shell端提交spark提交命令,一个常见的workcount提交命令如下
1
2
3
4
5
./spark-submit --master yarn-client \
--class com.example.spark.WordCount \
--executor-memory 1G \
--total-executor-cores 2 \
/opt/wordcount.jar hdfs://hacluster/aa/hello.tx

该命令表示像yarn提交一个spark程序进行运行,在shell端的执行流程如下。

  1. spark-submit 比较简单,直接将shell后面的参数转交给spark-class,并告诉spark-class以后运行的java类为 org.apache.spark.deploy.SparkSubmit。
1
exec "${SPARK_HOME}"/bin/spark-class org.apache.spark.deploy.SparkSubmit "$@"
  1. 此时程序在spark-class执行
  • spark-class会对环境变量做一些简单的查找和配置,如设置SPARK_HOME,SPARK_JARS_DIR,JAVA_HOME等
  • 调用build_command方法获取最终要执行的java命令,该命令会调用org.apache.spark.launcher.Main

build_command的代码如下

1
2
3
4
build_command() {
"$RUNNER" -Xmx128m -cp "$LAUNCH_CLASSPATH" org.apache.spark.launcher.Main "$@"
printf "%d\0" $?
}

该代码将shell后面的参数传入org.apache.spark.launcher.Main,获取执行结果

  1. org.apache.spark.launcher.Main,该类根据传入的参数,拼装classpath,jar等参数,返回最终要执行的java命令给spark-class
  • 输入:shell后面自带的参数,如
    –class com.example.spark.WordCount
    –executor-memory 1G
  • 输出我们看到的最终执行的java命令,如
    1
    2
    3
    4
    /opt/java/bin/java -cp /opt/spark/conf/:/opt/spark/jars/* \
    -Xmx1g org.apache.spark.deploy.SparkSubmit --master yarn \
    --class com.example.spark.WordCount --executor-memory 1G \
    --total-executor-cores 2 /opt/wordcount.jar hdfs://hacluster/aa/hello.tx
  • 执行过程
    Main类通过一个标准的建造者模式,传入参数,构建AbstractCommandBuilder
    1
    2
    3
    4
    AbstractCommandBuilder builder;
    if (className.equals("org.apache.spark.deploy.SparkSubmit")) {
    try {
    builder = new SparkSubmitCommandBuilder(args);
    最后传入env返回cmdList,通过对应系统的prepareCommand输出,由spark-class shell接收回参数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    List<String> cmd = builder.buildCommand(env);
    if (printLaunchCommand) {
    System.err.println("Spark Command: " + join(" ", cmd));
    System.err.println("========================================");
    }

    if (isWindows()) {
    System.out.println(prepareWindowsCommand(cmd, env));
    } else {
    // In bash, use NULL as the arg separator since it cannot be used in an argument.
    List<String> bashCmd = prepareBashCommand(cmd, env);
    for (String c : bashCmd) {
    System.out.print(c);
    System.out.print('\0');
    }
    }
  1. spark-class 执行最终的启动命令
    1
    2
    CMD=("${CMD[@]:0:$LAST}")
    exec "${CMD[@]}"

至此,在shell端的代码执行完毕,spark程序进入真正运行的java端代码