开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

linux之基础符号详解(中)

发表于 2021-11-14

「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战」

1.正则表达式(regular expression-RE)
练习的模拟环境:

1
2
3
4
csharp复制代码cat >>oldboy.txt<<EOF
I am oldboy!
I teach linux
EOF

基础正则表达式 - basic regular expression - BRE
1)^ — 以什么开头的信息找出来

1
2
3
4
5
6
7
8
9
csharp复制代码测验一:找出以m开头的文件内容信息
grep '^m' oldboy.txt

grep命令特点:按行进行操作处理(过滤)
如何给grep命令匹配出来的信息标记颜色
给grep命令设置一个别名
alias grep='grep --color=auto' 临时设置
echo "alias grep='grep --color=auto'">>/etc/profile 永久设置
[root@fu ~]# source /etc/profile 加载

2)$ — 找出以什么什么结尾的信息过滤出来

1
2
3
csharp复制代码测验练习二:找出以m结尾的内容信息
[root@fu ~]# grep 'm$' oldboy.txt
my blog is http://oldboy.blog.51cto.com

3)^$ – 找寻匹配空行信息(进行过滤)

测验练习题三:过滤空行信息

1
2
3
4
csharp复制代码[root@fu ~]# grep -v '^m$' oldboy.txt   -v == !

I am oldboy !
I teach linux

4). – 匹配任意一个字符信息且只能是一个字符信息(无法匹配空行信息)

1
csharp复制代码grep '.' oldboy.txt

5)* – 匹配0个或1次以上连续的字符信息(表示前一个字符连续出现1次或0次以上)

1
2
3
4
5
csharp复制代码grep '0*' oldboy.txt
什么叫匹配了0次??

会匹配出文件找不到内容,预支匹配
默认会将所有内容都显示出来

6).* — 匹配文件中的所有内容(可以匹配空行信息)
grep ‘.*‘ oldboy.txt

测验xx:请找出文件中以m开头,一行中所有信息

1
2
3
csharp复制代码[root@fu ~]# grep '^m.*' oldboy.txt

mygod ,i am not oldboy,but OLDBOY!

测验xx:请找出文件中以m开头,到m字符终止匹配

1
2
3
csharp复制代码[root@fu ~]# grep '^m.*m' oldboy.txt

mygod ,i am not oldboy,but!
  1. \ – 转移符号(中文–英文 英文–中文)
    作用:
    可以将有意义的符号,转换成没有意义 .
    可以将没有意义的符号,转换成有意义的 \n
    .(匹配任意字符)– . –加个撬棍一个点表示字符信息 – . –取消转译

测验练习题:将下面文件中一行信息,转换成一列显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
csharp复制代码01 02 03 04 05

01
02
03
04
05
tr "要进行替换的信息" "替换成什么信息" alex.txt

[root@fu ~]# tr " " "\n" <alex.txt 空格替换成换行符号
01
02
03
04
05
  1. [^abc] – 排除/不匹配 指定字符信息
1
2
3
4
5
6
csharp复制代码[root@fu ~]# grep "[^a-z]" oldboy.txt
I am oldboy !
I teach linux

[root@fu ~]# grep "^[^a-z]" oldboy.txt 不要以小写字母开头的句子
I am oldboy

测验xx:找出以.结尾的相应行的信息
grep ‘.$’ oldboy.txt

扩展正则表达式 - extended regular expression - ERE

    • — 匹配1个或1次以上连续的字符信息
1
2
3
4
csharp复制代码[root@fu ~]# egrep '0*' --color  oldboy.txt

I am oldboy !
[root@fu ~]# egrep '3*' --color oldboy.txt

9)[]+ 组合的用处

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
csharp复制代码[root@fu ~]# grep "[0-9]" oldboy.txt -o
5
1
4
9
0
0
0
4
4
8
4
9
0
0
0
0
0
4
4
8
[root@fu ~]# grep "[0-9]" oldboy.txt
my blog is sss
[root@fu ~]# grep "[0-9]+" oldboy.txt
[root@fu ~]# egrep "[0-9]+" oldboy.txt

not 4900000448

10)| 表示匹配多个满足条件的信息(彼此之间是或者关系)
egrep ‘o|y|t’ oldboy.txt

11)() 匹配一个整体信息(字符串信息) [a,usgduagd] 匹配中括号里面的一个一个元素信息

1
2
3
4
5
6
7
8
9
10
11
12
scss复制代码  表示匹配一个整体消息  egrep 'oldb(o|e)y' 表示过滤整体信息
表示用于后项引用前项 sed -r 's#(.*)#</1>#g' 利用sed命令实现后项引用前项
sed -r 's#([0-9]+)#<\1>#g'利用sed命令实现后项引用前项
sed -r 's#(12)(34)(56)#<\1><\2><\3>#g'利用sed命令实现后项引用前项



[root@fu ~]# egrep 'oldb[oe]y' oldboy.txt
I am oldboy teacher!
[root@fu ~]# egrep 'oldb(oe)y' oldboy.txt
[root@fu ~]# egrep 'oldb(o|e)y' oldboy.txt
I am oldboy teacher!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java环境变量配置以及eclipse中tomcat的安装

发表于 2021-11-14

「这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战」

JAVA环境配置

说到java环境,首先我们需要了解一下什么是环境变量

环境变量是在操作系统中一个具有特定名字的对象,它包含了一个或者多个应用程序所将使用到的信息。例如Windows和DOS操作系统中的path环境变量,当要求系统运行一个程序而没有告诉它程序所在的完整路径时,系统除了在当前目录下面寻找此程序外,还应到path中指定的路径去找。用户通过设置环境变量,来更好的运行进程。

配置环境变量的过程中我们需要用到JDK,JDK下载地址为www.oracle.com/technetwork…

image.png

打开环境变量界面以后 点击下方的新建

image.png

JAVA_HOME变量
新建JAVA_HOME变量

image.png

点击浏览目录,变量值为JDK安装目录,然后点击确定。

Path变量

找到系统变量中的path变量,选中,点击编辑

image.png

为了确保jre(运行)在你使用javaIDE的时候不产生问题,再新建一个变量

再次注意:必须全是英文状态下的字符。

点击确定,然后确定。打开path变量确认一下是否已经成功保存变量。

ClassPath变量

image.png

输入.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar;

点击确认,然后 关闭,重新打开,确认一下是否已经成功安装。

java运行环境的验证

敲击键盘的Windows + R 按键,打开运行提示框,输入cmd(command简写,打开“小黑框”)

输入java,验证java安装是否成功。

image.png

输入javac

image.png

验证完毕!

Eclipse开发JavaWeb项目配置Tomcat

首先我们需要了解一下什么是tomcat

Tomcat是apache旗下的一个开源项目,Tomcat 服务器是一个免费的开放源代码的Web 应用服务器,属于轻量级应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用,是开发和调试JSP 程序的首选。对于一个初学者来说,可以这样认为,当在一台机器上配置好Apache 服务器,可利用它响应HTML(标准通用标记语言下的一个应用)页面的访问请求。实际上Tomcat是Apache 服务器的扩展,但运行时它是独立运行的,所以当你运行tomcat 时,它实际上作为一个与Apache 独立的进程单独运行的。

tomcat下载地址为tomcat.apache.org

eclipse下载地址为www.eclipse.org/downloads/

下载完成后打开

image.png

再打开bin目录

image.png

找到startup.bat启动

image.png

启动成功

image.png

在浏览器中输入http://localhost:8080 进入如下页面表示安装成功

image.png

接下来再eclipse中配置tomcat

打开eclipse,单击Window菜单,选择下方的Preferences,找到Server选项

image.png

点开Runtime Environments,点击add

image.png

image.png

新建一个web项目

image.png

让tomcat服务器显示在控制台上,Window → Show View → Servers

image.png

点击eclipse下方区域的No servers are available. Click ths link to create a new server

在弹出的对话框中选择Tomcat版本

image.png

显示如图即启动成功

image.png

image.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MyBatis中使用foreach批量插入并且返回自增id

发表于 2021-11-14

「这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战」


  • 业务场景::
+ 签到表是个流程,等到审核完成后,需要将其从表数据自动添加至台账表中,方便台账表的导出功能的开发。所以我们得获取到批量插入的从表id,来维护主从表的关联关系。
  • 具体实现:
+ 建立实体类:因为返回的id会自动注入进去,所以得选实体类来接收自增的id。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Data
public class SignIn implements Serializable {
private long id;//注意id为long类型
private String owner;
private String modifier;
private String last_modified;
private String created_at;
private String modified_method;
private String app_key;
private String app_extend_key;
private String canjiayixiangxin;
private String canjiashijian;
private String canjiaren;
private String yanlianliushuihao;
}
  • mapper的编写:
    • 注意点:正常的插入只需要parameterType=“”即可。
    • 我们需要返回id的话得多加以下三个:
    • useGeneratedKeys=”true” :允许JDBC支持自动生成主键,需要驱动兼容(如果设置为true则这个设置强制使用自动生成主键,尽管一些驱动不能兼容但仍可正常工作)
    • keyProperty=”id” :取id的key值
    • keyColumn=”id”:设值id的值
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码<!--插入数据至台账从表-->
<insert id="insertParameter" useGeneratedKeys="true" keyProperty="id" keyColumn="id" >
insert into
${tableId}(
owner,modifier,last_modified,created_at,modified_method,app_key,app_extend_key, canjiayixiangxin,canjiashijian,canjiaren,yanlianliushuihao)
values
<foreach collection="list" index="index" item="ids" separator=",">
(
#{ids.owner},#{ids.modifier},#{ids.last_modified},#{ids.created_at},# {ids.modified_method},#{ids.app_key},#{ids.app_extend_key}, #{ids.canjiayixiangxin},#{ids.canjiashijian},#{ids.canjiaren},#{ids.yanlianliushuihao}
)
</foreach>
</insert>
  • service层:
  • 前提是已拿到所需要插入的数据集合了。
  • 注意点:网上大把博客都忽略了一个点,大家也会经常遇到一个问题就是,返回的id在集合中会重复,下面会说下为什么出现这样的原因。
  • *原因:* 本质上我们批量插入时,是将一个实体类组成的集合执行插入语句,用foreach进行批量插入。
  • 我们将List<Map<String,Object>> 类型转成List<实体类>。使用mapToBean方法来转换。
  • 因为我们是循环转换的,所以每次都得在重新new一个对象,不然id就会被覆盖。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码//如果数据为空,结束,不需要再执行下去
if (CollectionUtils.isEmpty(resultList)){
return;
}
//格式化时间
SimpleDateFormat sdfDay = new SimpleDateFormat("yyy-MM-dd HH:mm:ss");
List<SignIn> signInList = new ArrayList<>();
for (Map map : resultList){
map.put("created_at",sdfDay.format(map.get("created_at")));
map.put("canjiashijian",sdfDay.format(map.get("canjiashijian")));
map.put("last_modified",sdfDay.format(map.get("last_modified")));
map.remove("id");
//转为实体类,这里每次需要重新new一个对象,不然id会被覆盖成最后一个的id
SignIn signIn = new SignIn();
signInList.add(BeanUtils.mapToBean(map,signIn));
}
  • 这样即可拿到所有插入的自增id。
  • 下面提供下Map转实体的方法给大家:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class BeanUtils {

private BeanUtils() {throw new IllegalStateException();
}
/**
* map转bean
*
* @param map
* @param bean
* @param <T>
* @return
*/
public static <T> T mapToBean(Map<String, Object> map, T bean) {
BeanMap beanMap = BeanMap.create(bean);
beanMap.putAll(map);
return bean;
}
}

路漫漫其修远兮,吾必将上下求索~

如果你认为i博主写的不错!写作不易,请点赞、关注、评论给博主一个鼓励吧~hahah

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Elasticsearch 从cluster 到field

发表于 2021-11-14

一、是什么

Elasticsearch(ES) 是近实时、高性能、高弹性的分布式搜索和分析引擎,存储格式基于json,由apache lucene提供单机的搜索和存储。

二、基础语法

2.1 新建索引

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
67
68
69
70
71
72
73
json复制代码PUT juejin_hr_data_v1

{

"settings": {

"index": {

"routing": {

"allocation": {

"enable": "all"

}

},

"refresh_interval": "60s",

"number_of_shards": "3"

}

},

"mappings": {

"dynamic": false,

"properties": {

"week": {

"type": "keyword"

},

"team": {

"type": "keyword"

},

"school":{

"type":"integer"

},

"nowcoder":{

"type":"integer"

},

"boss":{

"type":"integer"

},

"maimai":{

"type":"integer"

}

}

}

}

2.2 基本增删改查

  1. 增加
1
2
3
4
5
json复制代码POST juejin_hr_data_v1/_bulk // bulk api,批量写入

{"index":{"_id":"client_1"}}

{"week":1,"team":"客户端","school":0,"nowcoder":0,"boss":0,"maimai":0}
  1. 删除
1
2
3
4
5
6
7
8
9
10
11
bash复制代码POST juejin_hr_data_v1/_delete_by_query

{

"query": {

"match_all": {}

}

}
  1. 更新
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
bash复制代码覆盖写

PUT juejin_hr_data_v1/_doc/1 //没有指定的index会创建

{

"week":1,"team":"客户端","school":0,"nowcoder":0,"boss":0,"maimai":0

}

PUT juejin_hr_data_v1/_create/1 //没有指定的index会报错

{

"week":1,"team":"客户端","school":0,"nowcoder":0,"boss":0,"maimai":0

}



更新部分字段

POST juejin_hr_data_v1/1/_update

{

"doc":{

"week":2

}

}
  1. 查询
1
2
3
4
5
6
7
8
9
10
11
bash复制代码GET juejin_hr_data_v1/_search

{

"query": {

"match_all": {}

}

}

三、整体架构

image.png

3.1 集群Cluster

  • Elasticsearch 集群由一个或多个节点(Node)组成,每个集群都有一个共同的集群名称作为标识。
  • Elasticsearch 实例即一个 Node,一台机器可以有多个实例,正常使用下每个实例应该会部署在不同的机器上。

3.1.1 集群属性

一个Elasticsearch集群的健康状态由Green、Yellow、Red三个枚举值来确认

  • Green:任取一个主分片/复制分片均可用
  • Yellow:存在复制分片不可用,任取一个主分片均可用
  • Red:存在主分片不可用

当集群状态为 red,它仍然正常提供服务,它会在现有存活分片中执行请求,需要尽快修复故障分片,防止查询数据的丢失;

es不像kafka和hbase依赖zookeeper,es有一套自己的集群维护机制

3.1.2 服务发现

es实现了自己的服务发现机制,称之为ZenDiscovery,服务发现是从单机节点形成集群的过程。当启动一个新的es节点或者主节点挂掉后,都会去触发服务发现的过程。

服务发现的起点是从多个host provider(settings、file、cloud等)提供的多个host(hostname+dns 或 ip)以及已知的具有选主资格的node节点开始。这个过程分成2个阶段:

阶段1. 每个节点尝试去连接各个种子节点(seed addresses)并确认连接的节点是符合要求的节点(有选主资格,master-eligible)

阶段2. 如果阶段1成功,则当前节点会与它这些连接的节点去共享当前节点已知的全部有资格的节点信息,同样远端节点也会一一去响应他已知的全部有选主资格的节点信息。这样当前节点就发现了一批新的节点,然后继续循环请求这些节点,直到整个集群构成了一张连通图,服务发现的过程就完成了。

如果当前节点不是有选主资格的节点,它会持续服务发现的过程,直到发现了被选举出的主节点,如果在本次服务发现的过程中没有发现主节点,节点会在discovery.find_peers_interval时间间隔后重试(默认是1秒)。

如果当前节点是有选主资格的节点,它会持续服务发现的过程直到它发现了一个被选举出的节点(elected master node)或者它发现了足够数量的不是主节点但有选主资格的节点去完成一次选举。如果这两个条件都不能满足,会在discovery.find_peers_interval后进行重试。

3.1.2 选主

ES是一种p2p(peer to peer)的分布式架构设计,集群中的每个节点都可以与其他任意节点进行通讯。这是不同于hadoop的master-slave的分布式系统。

ES中也存在master角色,但是其功能主要是维护集群的元信息(cluster status),当任意node上的信息修改时,将变更信息同步到其他剩余node上。也就是说每个node都具有一套完整的cluster status。

在es 7.0之后,es基于raft算法做了一些调整,并将其作为选主的实现。raft是工程上使用比较广泛的分布式的共识协议,即使在部分节点故障、网络延时、脑裂的情况下,依然可以多个节点对某个事情达成一致的看法。本文不做过多介绍,raft可以参考:深度解析 Raft 分布式一致性协议

3.2 节点 node

3.2.1 节点属性

  • Elasticsearch 的配置文件中可以通过 node.master、node.data 来设置节点类型,ES有多种节点类型,按照两个配置项,可枚举成四种,分别如下。
+ **主节点+数据节点(默认)**
1
2
3
yaml复制代码node.master: true 

node.data: true

节点即有成为主节点的资格,又存储数据。Elasticsearch 默认每个节点都是这样的配置

  • 主节点
1
2
3
yaml复制代码node.master: true 

node.data: false

不会存储数据,有成为主节点的资格,可以参与选举,有可能成为真正的主节点。普通服务器即可(CPU、内存消耗一般)。

  • 数据节点
1
2
3
yaml复制代码node.master: false 

node.data: true

节点没有成为主节点的资格,不参与选举,只会存储数据。在集群中需要单独设置几个这样的节点负责存储数据,后期提供存储和查询服务。主要消耗磁盘,内存。

  • 客户端节点
1
2
3
yaml复制代码node.master: false 

node.data: false

不会成为主节点,也不会存储数据,主要是针对请求进行分发。

3.3 分片 shard

  • 分片是 Elasticsearch 在集群中分发数据的关键。把分片想象成数据的容器。文档存储在分片中,然后分片分配到集群中的节点上。当集群扩容或缩小,Elasticsearch 将会在节点间迁移分片,以使集群保持平衡。
  • 分片可以是主 分片 (primary shard) 或者是复制 分片 (replica shard)

分片作为存储数据的单元,只存在于数据节点

复制分片只是主分片的一个副本,它可以防止硬件故障导致的数据丢失,同时可以提供读请求

3.2.1 分片备份

不支持在 Docs 外粘贴 block

在一个索引下,主分片会尽可能均匀的分布到每个节点中,而复制分片则不会分布到和主分片相同的实例。

Node2节点下线后,集群在短时间内会对分片进行重新分布,当然依赖遵循主、复制分片不会在同一个Node;如果Node1继续下线,所有主分片会集中在Node0,此时集群健康值:未连接。因为当前可用的主节点数 1 < discovery.zen.minimum_master_nodes 的默认值 2。

若把 discovery.zen.minimum_master_nodes 设置成 1,然后只启动一个节点,此时集群健康值:yellow 。这种情况下代表主分片全部可用,存在不可用的复制分片,5个复制分片没有分配到节点上,不过此时的集群是可用的,只是所有的操作都落到主分片上,而且可能引发单点故障。

3.2.2 写索引过程

  • ES 集群中每个节点通过路由都知道集群中的文档的存放位置,所以每个节点都有处理读写请求的能力。
  • 在一个写请求被发送到某个节点后,该节点即为协调节点,协调节点会根据路由公式计算出需要写到哪个分片上,再将请求转发到该分片的主分片节点上。假设 shard = hash(_routing) % num_of_pshard ,则过程大致如下:

  1. 客户端向 ES1节点发送写请求,通过路由计算公式得到值为0,则当前数据应被写到主分片 S0 上。
  2. ES1 节点将请求转发到 S0 主分片所在的节点 ES3,ES3 接受请求并写入。
  3. 并发将数据复制到两个副本分片 R0 上,其中通过乐观并发控制数据的冲突。一旦所有的副本分片都报告成功,则节点 ES3 将向协调节点报告成功,协调节点向客户端报告成功。

默认情况下会把doc的_id作为_routing的值,也可以手动指定routing的字符串,例如对于文章的场景,可以指定文章的标签作为routing的值,会将相同标签的文章写入同一个/组shard,在查询指定标签下的文章时,直接指定routing,会减少很大的查询量。(默认情况下ES查询会查询每一个shard再做合并)

3.3 分段 segment

3.3.1 索引不可变

  • 写入磁盘的倒排索引是不可变的,优势主要表现在:
+ 不需要锁。因为索引不可变,所以就不用担心多个请求使用时产生不一致的问题
  • 一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性,只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。
  • 其它缓存在索引的生命周期内始终有效,不需要在每次数据改变时被重建,因为数据不会变化。
  • 写入单个大的倒排索引,可以压缩数据,较少磁盘 IO 和需要缓存索引的内存大小。
  • 同样,因为索引可不变,所以在对旧数据编辑时会存在一些问题:
+ 当对旧数据进行删除时,旧数据不会马上被删除,而是在 `.del`文件中被标记为删除。而旧数据只能等到段更新时才能被移除,这样会造成大量的空间浪费。
+ 若有一条数据频繁的更新,每次更新都是新增新的标记旧的,则会有大量的空间浪费。
+ 每次新增数据时都需要新增一个段来存储数据。当段的数量太多时,对服务器的资源例如文件句柄的消耗会非常大。
+ 在查询的结果中包含所有的结果集,需要排除被标记删除的旧数据,这增加了查询的负担。

所以,既要保证索引不变时的效率,又要尽可能避免因此产生的问题,那么就引入的段(Segment)

3.3.2 分段

  • 索引文件被拆分为多个子文件,每个子文件叫作段, 每一个段本身都是一个倒排索引,并且段具有不变性,一旦索引的数据被写入硬盘,就不可再修改。

3.3.2 段的更新

  • 新增,段的新增很好处理,由于数据是新的,所以只需要对当前文档新增一个段即可。
  • 删除,由于不可修改,所以对于删除操作,不会把文档从旧的段中移除,而是通过新增一个 .del文件,包含了段上已经被删除的文档。当一个文档被删除,它实际上只是在.del文件中被标记为删除,依然可以匹配查询,但是最终返回之前会被从结果中删除
  • 更新,不能修改旧的段来进行反映对文档的更新,其实更新相当于是删除和新增这两个动作组成。会将旧的文档在 .del文件中标记删除,然后文档的新版本被索引到一个新的段中。可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就会被移除。

3.3.3 段的合并

  • 因为对分段新增、删除及更新的特殊处理,会产生过多的段。而过多的段除了耗费文件句柄,内存,cpu资源之外,还会因为搜索时检索的段过多而拖慢查询时间。
  • 所以当段的数量过多时,会将各个小段合并为一个大的段。段合并的主要操作如下:
+ 新的段flush到了硬盘。
+ 新的提交点写入新的段,排除旧的段。
+ 新的段打开供搜索。
+ 旧的段被删除。

3.3.4 reflesh

ES 是怎么做到近实时全文搜索?

  • 磁盘是瓶颈。提交一个新的段到磁盘的操作开销较大,严重影响性能,当写数据量大的时候会造成 ES 停顿卡死,查询也无法做到快速响应。
  • 所以持久化过程不能在每个文档被索引的时就触发,需要一种更轻量级的方式使新的文档可以被搜索。为了提升写的性能,ES 没有每新增一条数据就增加一个段到磁盘上,而是采用延迟写的策略。
  • 每当有新增的数据时,就将其先写入到内存中,在内存和磁盘之间是文件系统缓存,当达到默认的时间(1秒钟)或者内存的数据达到一定量时,会触发一次刷新(Refresh),将内存中的数据生成到一个新的段上并缓存到文件缓存系统上,稍后再被刷新到磁盘中并生成提交点。
  • 新的数据会继续的被写入内存,但内存中的数据并不是以段的形式存储的,因此不能提供检索功能。由内存刷新到文件缓存系统的时候会生成了新的段,并将段打开以供搜索使用,而不需要等到被刷新到磁盘。
  • 在 Elasticsearch 中,这种写入和打开一个新段的轻量的过程就是reflesh,默认情况下每个分片会每秒自动刷新一次。

3.3.5 flush

虽然通过定时 Refresh 获得近实时的搜索,但是 Refresh 只是将数据挪到文件缓存系统,没有对数据进行持久化。为了避免丢失数据,Elasticsearch添加了Translog,事务日志记录了所有还没有持久化到磁盘的数据。整个flush过程如下:

  1. 当一个文档被索引,它被加入到内存缓存,同时加到事务日志。不断有新的文档被写入到内存,同时也都会记录到事务日志中。这时新数据还不能被检索和查询。

  1. 当达到默认的刷新时间或内存中的数据达到一定量后,会触发一次 refresh:
  • 内存缓冲区的文档写入到段中,但没有fsync。
  • 段被打开,使得新的文档可以搜索。
  • 缓存被清除

  1. 随着更多的文档加入到缓存区,写入日志,这个过程会继续

  1. 随着新文档索引不断被写入,当日志数据大小超过 512M 或时间超过 30 分钟时,会进行一次全提交
  • 内存缓存区的所有文档会写入到新段中
  • 清除缓存
  • 一个提交点写入硬盘
  • 文件系统缓存通过fsync操作flush到硬盘
  • 事务日志被清除

3.4 文档 doc

3.4.1 schema

schema,对应到es,其实就是mapping,es数据的交互形式是json,doc可以做到开箱即用,在写入doc时如果没有预先定义的mapping,doc的每一个field会根据传过来的json数据确定类型,默认规则(dynamic field mapping)如下:

json类型 es类型
null 不会增加field
boolean boolean
string date(通过Date detection)double/long(通过Numeric detection)text(带keyword的sub field)
number float/long
object Object
array array(array的item类型取决于第一个非null元素的类型)

同时es还支持定义模板 dynamic_template,来对默认的规则进行扩充、修改,例如下例就是修改了默认的string映射,意思是在匹配到string类型后,使用的es类型为text:

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
markdown复制代码{

"mappings": {

"dynamic_templates": [

{

"strings_as_keywords": {

"match_mapping_type": "string",

"mapping": {

"type": "text"

}

}

}

]

}

不过如果没有动态字段的需求,个人不建议使用es的dynamic mapping,使用不当的话会污染mapping,所以可以指定dynamic为false来关闭动态mapping。

当然可以使用put mapping api来预定义index的mapping结构,包括字段类型、使用的分析器(text类型)、是否索引等等。

es官方也非常推荐将相同的字段以不同的方式索引到es中,例如一个字符串类型的值可以使用索引成text类型来进行全文检索,也可以索引成keyword类型进行排序、聚合。

建议使用别名(alias),es对mapping的拓展是开放的,但对mapping的修改是禁止的。例如,可以为mapping增加一个字段,但是不能删除/修改字段。所以使用alias指向真正的index,这样,在有field需要修改的场景可以使用reindex api重建索引、再使用alias api更改指向,可以实现无缝的切换。

3.4.2 metadata

es中每个doc都会有一些关联的元数据,如下:

  1. _index,当前doc所属的index
  2. _type,当前doc的mapping type
  3. _id,doc的唯一标识,index内唯一
  4. _source,doc的json原始数据
  5. _size,doc的长度
  6. _rounting,上文介绍过的自定义路由的值

3.5 字段 field

field是ES中最小的数据单位。默认情况下,es会索引每个字段中的所有数据,每个字段类型都会有专有的经过优化过后的数据类型,比如,字符串类型(例如text和keyword)倒排索引进行存储,数据类型(例如interger、float等)则会使用BKD tree进行索引存储。对不同的字段类型使用不同的索引方式,这也是ES为什么这么快的原因之一。

3.4.1 字段类型

es支持如下字段类型

string text 使用配置的分析(分词)器对原始串做加工后写入倒排
keyword 直接将字段作为词根存入倒排
number long 取值范围: -2^63~2^63-1
long 取值范围:-2^31~2^31-1
short 取值范围:-32,768~32,767
byte 取值范围:-128~127
double 双精度浮点数
float 单精度浮点数
half_float 16位长浮点数
date string string格式的时间
number number格式的时间,一般是unix时间戳(毫秒/秒)
boolean boolean true / false
binary binary 二进制数据
object array 数组
range 范围
object json对象
nested 有关联关系的json对象,默认的object对象的各个字段都是打平的
geo 地图坐标
ip ipv4或ipv6
completion 自动补全类型,底层使用字典树索引
…. …

3.4.2 倒排索引

倒排索引(英语:Inverted index),也常被称为反向索引、置入文件或反向文件,是一种索引方法,被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射。它是文档检索系统中最常用的数据结构。 —-摘自维基百科

而ES最基础的索引结构就是倒排,举个例子来介绍下倒排,有三个文档,分别是:

doc 0:it is what it is

doc 1:what is it

doc 2:it is a banana

与之对应的倒排如下:

1
2
3
4
5
6
7
8
9
css复制代码 "a":      {2}

"banana": {2}

"is": {0, 1, 2}

"it": {0, 1, 2}

"what": {0, 1}

对应到es中,倒排的key就是文本串,倒排的value就是doc id list。

3.4.3 FST

FST(Finite State Transducer),有限状态自动机,类似trie树。

es使用FST数据结构来存储倒排的term字典,参考下图。

3.4.4 跳表 skiplist

es使用skiplist来来存储倒排value的doc id列表,方便对docid做检索。

在and条件中,一次query会涉及对多条倒排链的合并,基本合并规则如下,假设有3条倒排链

  1. 在termA开始遍历,得到第一个元素docId=$docid
  2. Set currentDocId=$docid
  3. 循环 advance(currentDocId) = 1 (返回大于等于currentDocId的一个doc),
1. 因为currentDocId ==1,继续
2. 如果currentDocId 和返回的不相等,执行2,然后继续
3. 如果相等的次数等于倒排链-1,则将docid加入结果集,取当前倒排链的next作为新的docid加入结果集,取当前倒排链的next作为新的docid加入结果集,取当前倒排链的next作为新的docid
  1. 直到某个倒排链到末尾。

3.4.5 BKD tree索引

Bkd树是一种动态索引数据结构,能高效且可伸缩地索引大的多维点数据集。它有 (1) 极高的空间利用率和 (2) 优秀的查询、(3) 更新性能——且这三种属性在高强度更新下依旧成立。

四、总结

es的内容还是非常多的,本文是从物理/逻辑上的粒度进行了拆分,从cluster依次到field进行了讲解。

参考文档:

www.cnblogs.com/caoweixiong…

www.cnblogs.com/duanxz/p/52…

www.elastic.co/guide/en/el…

www.cppblog.com/mysileng/ar…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

使用 Nginx NJS 实现高性能的 RSA 加解密服务

发表于 2021-11-14

在之前的文章《编写 Nginx 模块进行 RSA 加解密》中,我提到了如何编写 Nginx 模块,并借助 Nginx 实现相对高性能的加解密。正巧 Nginx 新版本发布,初步具备了原生“RSA加解密”能力。

那么,就来换一种更轻量的方式进行实现之前提到的功能吧。

写在前面

随着 Nginx 版本来到了 1.21.4 ,NJS 也升级到了 0.7 版本。这个版本可以说是具有突破意义的版本,因为这个版本的 NJS 添加了符合 W3C 的标准的 WebCrypto API。

这意味着,以往需要单独起一套服务来说接口加密鉴权的时代或许可以过去了。

官方实现这个功能主要是通过添加 njs_webcrypto.c 加解密模块,引入了部分 OpenSSL 的能力。如果你的需求包含了针对指定的 RSA 密钥(带密码)的加解密,那么目前 NJS 还做不到。不过,你可以修改上面的代码,添加我在《编写 Nginx 模块进行 RSA 加解密》一文中,提到的“计算部分”的代码实现:将 PEM_read_bio_RSAPrivateKey 携带密码的部分添加进去,并对 NJS 做一些函数绑定,最后记得清理干净 RSA 相关引用就好了。

好在在多数情况下,考虑到调用性能,针对业务接口进行加解密,不太倾向使用添加密码的密钥。

接下来,我将介绍如何使用 Nginx NJS 的这个新能力,一步步的实现一个能够根据业务接口内容,进行 RSA 自动加解密的接口服务。

使用浏览器生成 RSA 证书

你没有看错小标题,这次我们要使用浏览器而不是“传统命令行中OpenSSL”来生成我们的证书。

这里主要会用到两个 API:

  • SubtleCrypto.generateKey()
  • SubtleCrypto.exportKey()

文档枯燥,这里直接划出重点。在生成算法中,本文采用 WEB Crypto API 唯一支持的非对称加密算法 RSA-OAEP,在导出生成证书时,需要根据密钥类型,针对性的选择对应的导出格式。

从浏览器中生成和导出的 RSA 密钥对

为了方便我的读者玩耍,我写了一段简单的 JavaScript 脚本,将内容复制粘贴到你的浏览器控制台里(推荐 Chrome ),然后执行即可。不出意外,你的浏览器将会自动下载到两个名为 “rsa.pub”和“rsa.key”文件,我们稍后会使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码(async () => {
const ab2str = (buffer) => String.fromCharCode.apply(null, new Uint8Array(buffer));
const saveFile = async (files) => {
Object.keys(files).forEach(file => {
const blob = new Blob([files[file]], { type: 'text/plain' });
with (document.createElement('a')) { download = file; href = URL.createObjectURL(blob); click(); }
URL.revokeObjectURL(blob);
});
}
const exportKey = (content) => new Promise(async (resolve) => { await crypto.subtle.exportKey(content.type === "private" ? "pkcs8" : "spki", content).then((data) => resolve(`-----BEGIN ${content.type.toUpperCase()} KEY-----\n${btoa(ab2str(data))}\n-----END ${content.type.toUpperCase()} KEY-----`)); });
const { privateKey, publicKey } = await crypto.subtle.generateKey({ name: "RSA-OAEP", modulusLength: 4096, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" }, true, ["encrypt", "decrypt"])
saveFile({ "rsa.key": await exportKey(privateKey), "rsa.pub": await exportKey(publicKey) });
})();

使用 NJS 进行 RSA 加解密

虽然 Nginx 和 NJS 官方文档中,还未提及新添加的 WEB Crypto API 如何使用,但是我们可以从代码仓库中最新的测试用例中看到接口的用法。

我们参考之前的文章《使用 Docker 和 Nginx NJS 实现 API 聚合服务(前篇)》中“使用 NJS 编写 Nginx 基础接口”的代码为基础,先写一个“糙一些”的版本出来,体验下使用 NJS 进行 Nginx 原生 RSA 加解密:

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
js复制代码const fs = require('fs');
if (typeof crypto == 'undefined') {
crypto = require('crypto').webcrypto;
}

function pem_to_der(pem, type) {
const pemJoined = pem.toString().split('\n').join('');
const pemHeader = `-----BEGIN ${type} KEY-----`;
const pemFooter = `-----END ${type} KEY-----`;
const pemContents = pemJoined.substring(pemHeader.length, pemJoined.length - pemFooter.length);
return Buffer.from(pemContents, 'base64');
}

const rsaKeys = {
public: fs.readFileSync(`/etc/nginx/script/rsa.pub`),
private: fs.readFileSync(`/etc/nginx/script/rsa.key`)
}

async function simple(req) {

const spki = await crypto.subtle.importKey("spki", pem_to_der(rsaKeys.public, "PUBLIC"), { name: "RSA-OAEP", hash: "SHA-256" }, false, ["encrypt"]);
const pkcs8 = await crypto.subtle.importKey("pkcs8", pem_to_der(rsaKeys.private, "PRIVATE"), { name: "RSA-OAEP", hash: "SHA-256" }, false, ["decrypt"]);

let originText = "假设这是需要加密的内容,by soulteary";

let enc = await crypto.subtle.encrypt({ name: "RSA-OAEP" }, spki, originText);
let decode = await crypto.subtle.decrypt({ name: "RSA-OAEP" }, pkcs8, enc);

req.headersOut["Content-Type"] = "text/html;charset=UTF-8";
req.return(200, [
'<h2>原始内容</h2>',
`<code>${originText}</code>`,
'<h2>加密后的内容</h2>',
`<code>${Buffer.from(enc)}</code>`,
'<h2>解密后的内容</h2>',
`<code>${Buffer.from(decode)}</code>`,
].join(''));
}

export default { simple };

上面的代码定义了一个简单的接口“simple”,用于加载我们刚刚生成的 RSA Keys,然后对一段指定的内容(originText)进行加密再解密。将上面的内容保存为 app.js,我们继续编写一段简单的 Nginx 配置(nginx.conf):

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
bash复制代码load_module modules/ngx_http_js_module.so;

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;

events { worker_connections 1024; }

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

js_import app from script/app.js;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;

keepalive_timeout 65;
gzip on;

server {
listen 80;
server_name localhost;

charset utf-8;
gzip on;

location / {
js_content app.simple;
}
}
}

为了使用简单,这里同样给出一份容器配置(docker-compose.yml):

1
2
3
4
5
6
7
8
9
10
11
yaml复制代码version: '3'

services:

nginx-rsa-demo:
image: nginx:1.21.4-alpine
ports:
- 8080:80
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./scripts:/etc/nginx/script

使用 docker-compose up 启动容器,然后在浏览器中访问 localhost:8080,可以看到下面的内容。

使用 Nginx NJS 针对内容进行 RSA 加解密

顺便看一下响应时间,在笔记本的容器里大概十来ms,如果放到生产环境,加上一些优化,控制在个位数里问题不大。

接口响应时间

好了,能力验证到此就结束了。我们来稍加改造和优化,实现网关产品中的全自动的 RSA 加解密功能。

构建具备 RSA 加解密能力的网关

下面具体实战一下,如何使用 Nginx 的 NJS 针对请求进行加解密。先来编写 Nginx 配置部分。

调整 Nginx 配置使用的 NJS 导出函数

考虑到调试方便,我们将“入口点”(接口)拆分为三个,你可以根据实际使用场景进行调整,比如在入口处添加 IP 访问限制、额外的身份验证功能,或者取消“统一的入口”,直接使用两个主要的加解密接口为程序“入口点”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bash复制代码server {
listen 80;
server_name localhost;

charset utf-8;
gzip on;

location / {
js_content app.entrypoint;
}

location /api/encrypt {
js_content app.encrypt;
}

location /api/decrypt {
js_content app.decrypt;
}
}

完成了 Nginx 配置的编写后,就可以开始正餐了:编写 NJS 程序。

调整 NJS 程序:调整导出函数

Nginx 配置修改之后,同样的, NJS 中的导出函数也需要进行调整:

1
bash复制代码export default { encrypt, decrypt, entrypoint };

修改完毕导出函数后,我们依次来实现三个接口函数的功能。

实现 NJS 程序:默认入口函数

因为目前 NJS 的开发调试还处于非常不方便的状态,所以我们先来编写入口函数,以方便调试过程(app.js):

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
bash复制代码function debug(req) {
req.headersOut["Content-Type"] = "text/html;charset=UTF-8";
req.return(200, JSON.stringify(req, null, 4));
}

function encrypt(req) {
debug(req)
}

function decrypt(req) {
debug(req)
}

function entrypoint(r) {
r.headersOut["Content-Type"] = "text/html;charset=UTF-8";
switch (r.method) {
case 'GET':
return r.return(200, [
'<form action="/" method="post">',
'<input name="data" value=""/>',
'<input type="radio" name="action" id="encrypt" value="encrypt" checked="checked"/><label for="encrypt">Encrypt</label>',
'<input type="radio" name="action" id="decrypt" value="decrypt"/><label for="decrypt">Decrypt</label>',
'<button type="submit">Submit</button>',
'</form>'
].join('<br>'));
case 'POST':
var body = r.requestBody;
if (r.headersIn['Content-Type'] != 'application/x-www-form-urlencoded' || !body.length) {
r.return(401, "Unsupported method\n");
}

var params = body.trim().split('&').reduce(function (prev, item) {
var tmp = item.split('=');
var key = decodeURIComponent(tmp[0]).trim();
var val = decodeURIComponent(tmp[1]).trim();
if (key === 'data' || key === 'action') {
if (val) {
prev[key] = val;
}
}
return prev;
}, {});

if (!params.action || (params.action != 'encrypt' && params.action != 'decrypt')) {
return r.return(400, 'Invalid Params: `action`.');
}

if (!params.data) {
return r.return(400, 'Invalid Params: `data`.');
}

function response_cb(res) {
r.return(res.status, res.responseBody);
}

return r.subrequest(`/api/${params.action}`, { method: 'POST' }, response_cb)
default:
return r.return(400, "Unsupported method\n");
}
}

export default { encrypt, decrypt, entrypoint };

上面60来行代码中,我们实现了哪些功能呢?

  • 一个简单的 Web 表单界面,用于接收我们调试开发过程中的“加解密动作”、“需要加解密的数据”。
  • 根据我们选择的动作,自动进行“加解密”操作,并返回具体加解密接口的处理结果。
  • 简单 Mock 了加解密接口,目前实际调用一个名为 debug 的函数打印我们的提交内容。

使用浏览器访问界面,能够看到这个简单的提交界面:

使用 NJS 制作的简单调试页面

在调试表单里的文本框中随便写一点内容,进行提交,可以看到函数运行符合预期,提交内容被正确的打印了出来:

函数运行符合预期

接着,我们来实现 NJS 的 RSA 加密函数。

实现 NJS 程序:RSA 加密函数

参考前文,稍作调整,不难实现这个加密函数,大概五行左右就够了。

1
2
3
4
5
js复制代码async function encrypt(req) {
const spki = await crypto.subtle.importKey("spki", pem_to_der(rsaKeys.public, "PUBLIC"), { name: "RSA-OAEP", hash: "SHA-256" }, false, ["encrypt"]);
const result = await crypto.subtle.encrypt({ name: "RSA-OAEP" }, spki, req.requestText);
req.return(200, Buffer.from(result));
}

再次运行 Nginx ,提交内容,可以看到数据已经被顺利的进行了 RSA 加密。

NJS RSA 加密函数默认输出

因为 RSA 默认加密后的内容不具备可读性,所以一般情况下,如果明文传输,我们会套一层 Base64 来展示。所以,我们需要对这个函数以及上一步中的函数进行一些调整,先拿入口函数“开刀”。

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
js复制代码function entrypoint(r) {
r.headersOut["Content-Type"] = "text/html;charset=UTF-8";

switch (r.method) {
case 'GET':
return r.return(200, [
'<form action="/" method="post">',
'<input name="data" value=""/>',
'<input type="radio" name="action" id="encrypt" value="encrypt" checked="checked"/><label for="encrypt">Encrypt</label>',
'<input type="radio" name="action" id="decrypt" value="decrypt"/><label for="decrypt">Decrypt</label>',
'<input type="radio" name="base64" id="base64-on" value="on" checked="checked"/><label for="base64-on">Base64 On</label>',
'<input type="radio" name="base64" id="base64-off" value="off" /><label for="base64-off">Base64 Off</label>',
'<button type="submit">Submit</button>',
'</form>'
].join('<br>'));
case 'POST':
var body = r.requestBody;
if (r.headersIn['Content-Type'] != 'application/x-www-form-urlencoded' || !body.length) {
r.return(401, "Unsupported method\n");
}

var params = body.trim().split('&').reduce(function (prev, item) {
var tmp = item.split('=');
var key = decodeURIComponent(tmp[0]).trim();
var val = decodeURIComponent(tmp[1]).trim();
if (key === 'data' || key === 'action' || key === 'base64') {
if (val) {
prev[key] = val;
}
}
return prev;
}, {});

if (!params.action || (params.action != 'encrypt' && params.action != 'decrypt')) {
return r.return(400, 'Invalid Params: `action`.');
}

if (!params.base64 || (params.base64 != 'on' && params.base64 != 'off')) {
return r.return(400, 'Invalid Params: `base64`.');
}

if (!params.data) {
return r.return(400, 'Invalid Params: `data`.');
}

function response_cb(res) {
r.return(res.status, res.responseBody);
}

return r.subrequest(`/api/${params.action}${params.base64 === 'on' ? '?base64=1' : ''}`, { method: 'POST', body: params.data }, response_cb)
default:
return r.return(400, "Unsupported method\n");
}
}

我们在调试入口添加了一个是否开启 Base64 编码的选项,并在开启 Base64 编码的情况下,调用加解密接口的时候,额外添加了一个 ?base64=1 的请求参数。

加密函数的改造也很简单,差不多十行就行了:

1
2
3
4
5
6
7
8
9
10
11
js复制代码async function encrypt(req) {
const needBase64 = req.uri.indexOf('base64=1') > -1;
const spki = await crypto.subtle.importKey("spki", pem_to_der(rsaKeys.public, "PUBLIC"), { name: "RSA-OAEP", hash: "SHA-256" }, false, ["encrypt"]);
const result = await crypto.subtle.encrypt({ name: "RSA-OAEP" }, spki, req.requestText);
if (needBase64) {
req.return(200, Buffer.from(result).toString("base64"));
} else {
req.headersOut["Content-Type"] = "application/octet-stream";
req.return(200, Buffer.from(result));
}
}

重启 Nginx 服务,选择使用 Base64 编码,可以看到输出结果已经符合预期了。

Base64 化之后的NJS RSA 加密函数默认输出

将内容复制保存,稍后使用。我们来接着实现 RSA 解密功能。

实现 NJS 程序:RSA 解密函数

有了 RSA 加密函数,写出解密函数就更简单了,这里就不和加密函数一样,拆解步骤了,直接照顾到“是否启用 Base64”这个选项类型就好。

1
2
3
4
5
6
7
js复制代码async function decrypt(req) {
const needBase64 = req.uri.indexOf('base64=1') > -1;
const pkcs8 = await crypto.subtle.importKey("pkcs8", pem_to_der(rsaKeys.private, "PRIVATE"), { name: "RSA-OAEP", hash: "SHA-256" }, false, ["decrypt"]);
const encrypted = needBase64 ? Buffer.from(req.requestText, 'base64') : Buffer.from(req.requestText);
const result = await crypto.subtle.decrypt({ name: "RSA-OAEP" }, pkcs8, encrypted);
req.return(200, Buffer.from(result));
}

使用上一步里的 Base64 后的 RSA 加密结果进行提交,可以看到我们前文中加密的内容就能被正确解密了。

NJS 计算 RSA 解密结果

有了上面的基础,接下来我们来折腾自动化加解密。

构建具备自动加解密能力的网关

为了模拟真实业务场景,我们得分别调整 Nginx 配置、容器配置。

调整 Nginx 配置:模拟业务接口

还是先进行 Nginx 配置的调整。

先模拟两个新的服务,并设定它们输出的内容,分别为原始数据和已被 RSA 加密过的数据。为了保持简单,我们还是使用 NJS 来模拟服务端接口响应内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bash复制代码server {
listen 8081;
server_name localhost;

charset utf-8;
gzip on;

location / {
js_content mock.mockEncData;
}
}

server {
listen 8082;
server_name localhost;

charset utf-8;
gzip on;

location / {
js_content mock.mockRawData;
}
}

为了在模拟服务中使用 NJS,记得在 Nginx 全局配置中添加额外的 NJS 脚本引用声明:

1
bash复制代码js_import mock from script/mock.js;

为了方便本地调试,我们还可以调整容器编排配置,将上面两个服务的接口公开出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
yaml复制代码version: '3'

services:

nginx-api-demo:
image: nginx:1.21.4-alpine
restart: always
ports:
- 8080:80
- 8081:8081
- 8082:8082
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./scripts:/etc/nginx/script

实现 NJS 程序:编写业务模拟接口

这里参考上文,可以迅速写出两个业务接口,它们分别会输出后续需要加密原始数据,以及RSA加密后的数据。为了模拟真实场景,这里使用随机函数,随机的针对三个不同的内容进行具体计算。

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
js复制代码function randomPick() {
const powerWords = ['苏洋博客', '专注硬核', '分享有趣'];
return powerWords[Math.floor(Math.random() * powerWords.length)];
}

function mockRawData(r) {
r.headersOut["Content-Type"] = "text/html;charset=UTF-8";
r.return(200, randomPick());
}

const fs = require('fs');
if (typeof crypto == 'undefined') {
crypto = require('crypto').webcrypto;
}

function pem_to_der(pem, type) {
const pemJoined = pem.toString().split('\n').join('');
const pemHeader = `-----BEGIN ${type} KEY-----`;
const pemFooter = `-----END ${type} KEY-----`;
const pemContents = pemJoined.substring(pemHeader.length, pemJoined.length - pemFooter.length);
return Buffer.from(pemContents, 'base64');
}

const publicKey = fs.readFileSync(`/etc/nginx/script/rsa.pub`);

async function mockEncData(r) {
const spki = await crypto.subtle.importKey("spki", pem_to_der(publicKey, "PUBLIC"), { name: "RSA-OAEP", hash: "SHA-256" }, false, ["encrypt"]);
const result = await crypto.subtle.encrypt({ name: "RSA-OAEP" }, spki, randomPick());

r.headersOut["Content-Type"] = "text/html;charset=UTF-8";
r.headersOut["Encode-State"] = "ON";
r.return(200, Buffer.from(result).toString("base64"));
}

export default { mockEncData, mockRawData };

一切就绪之后,我们访问不同的端口,可以看到“业务接口”已经就绪啦。这里通过对已加密的数据添加 Encode-State 请求头,来做数据类型区别。如果你不希望添加额外字段,也可以在 Content-Type 中进行响应数据类型标识。

使用 NJS  模拟业务接口

调整网关 Nginx 配置:聚合业务接口

业务实际使用方法有两种,一种是业务接口调用我们前文中的网关加解密功能,进行数据加解密,然后进行响应。而另外一种,则是网关聚合业务接口,根据数据响应类型调整对应的输出结果。

本文选择后一种方案,搭配 Traefik 可以实现快速的水平扩容,以提高服务响应能力。

因为 NJS 的子请求有请求来源限制,为了能够和业务数据进行交互,需要在网关的 Nginx 配置中添加两个接口,代理远端的需要加密或解密的业务数据。

1
2
3
4
5
6
7
bash复制代码location /remote/need-encrypt {
proxy_pass http://localhost:8082/;
}

location /remote/need-decrypt {
proxy_pass http://localhost:8081/;
}

配置完毕,你就可以通过 http://localhost:8080/remote/need-encrypt 和 http://localhost:8080/remote/need-encrypt 访问上一小节中的内容了。

同时,为了我们能够访问自动加解密的接口,还需要再添加一个接口,用于调用 NJS 函数进行数据的自动加解密。(实际业务使用,追求极致性能,可以考虑拆分成两个)

1
2
3
bash复制代码location /auto{
js_content app.auto;
}

实现 NJS 程序:自动加解密业务数据

我们先来实现一个能够根据我们指定的数据源(加密过的数据、未解密的数据),进行数据的自动处理。

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
js复制代码async function auto(req) {
req.headersOut["Content-Type"] = "text/html;charset=UTF-8";

let remoteAPI = "";
switch (req.args.action) {
case "encrypt":
remoteAPI = "/remote/need-encrypt";
break;
case "decrypt":
default:
remoteAPI = "/remote/need-decrypt";
break;
}

async function autoCalc(res) {
const isEncoded = res.headersOut['Encode-State'] == "ON";
const remoteRaw = res.responseText;
if (isEncoded) {
const pkcs8 = await crypto.subtle.importKey("pkcs8", pem_to_der(rsaKeys.private, "PRIVATE"), { name: "RSA-OAEP", hash: "SHA-256" }, false, ["decrypt"]);
const encrypted = Buffer.from(remoteRaw, 'base64');
const result = await crypto.subtle.decrypt({ name: "RSA-OAEP" }, pkcs8, encrypted);
req.return(200, Buffer.from(result));
} else {
const spki = await crypto.subtle.importKey("spki", pem_to_der(rsaKeys.public, "PUBLIC"), { name: "RSA-OAEP", hash: "SHA-256" }, false, ["encrypt"]);
const dataEncrypted = await crypto.subtle.encrypt({ name: "RSA-OAEP" }, spki, remoteRaw);
req.return(200, Buffer.from(dataEncrypted).toString("base64"));
}
}

req.subrequest(remoteAPI, { method: "GET" }, autoCalc)
}


export default { encrypt, decrypt, entrypoint, auto };

重启 Nginx ,分别访问代理远端数据接口 /remote/need-encrypt 和自动加密的网关接口,可以看到程序已经能够符合预期的运行了。

NJS 根据请求自动加密业务接口数据

为了让程序更智能一些,达到数据加解密的完全自动化,可以再进行一个简单调整,让程序不是根据我们指定的参数去访问原始数据,而是随机访问原始数据。(为了能够直观验证行为,这里我们将输出内容也进行调整)

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
js复制代码async function auto(req) {
req.headersOut["Content-Type"] = "text/html;charset=UTF-8";

function randomSource() {
const sources = ["/remote/need-encrypt", "/remote/need-decrypt"];
return sources[Math.floor(Math.random() * sources.length)];
}

async function autoCalc(res) {
const isEncoded = res.headersOut['Encode-State'] == "ON";
const remoteRaw = res.responseText;
if (isEncoded) {
const pkcs8 = await crypto.subtle.importKey("pkcs8", pem_to_der(rsaKeys.private, "PRIVATE"), { name: "RSA-OAEP", hash: "SHA-256" }, false, ["decrypt"]);
const encrypted = Buffer.from(remoteRaw, 'base64');
const result = await crypto.subtle.decrypt({ name: "RSA-OAEP" }, pkcs8, encrypted);
req.return(200, [
"<h2>原始内容</h2>",
`<code>${remoteRaw}</code>`,
"<h2>处理后的内容</h2>",
`<code>${Buffer.from(result)}</code>`
].join(""));
} else {
const spki = await crypto.subtle.importKey("spki", pem_to_der(rsaKeys.public, "PUBLIC"), { name: "RSA-OAEP", hash: "SHA-256" }, false, ["encrypt"]);
const dataEncrypted = await crypto.subtle.encrypt({ name: "RSA-OAEP" }, spki, remoteRaw);
req.return(200, [
"<h2>原始内容</h2>",
`<code>${remoteRaw}</code>`,
"<h2>处理后的内容</h2>",
`<code>${Buffer.from(dataEncrypted).toString("base64")}</code>`
].join(""));
}
}

req.subrequest(randomSource(), { method: "GET" }, autoCalc)
}

再次重启 Nginx ,多刷新几次,就能看到根据内容自动进行 RSA 加解密的结果啦。

NJS 实现 RSA 内容自动加解密

其他:接口安全考虑

实际使用过程中,除了推荐在业务前添加额外的鉴权验证、频率限制外,同样建议根据实际情况使用 internal 限制 Nginx 接口的“作用域”,让数据源和基础计算接口仅允许被 NJS 程序内部访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
js复制代码location /remote/need-encrypt {
internal;
proxy_pass http://localhost:8082/;
}

location /remote/need-decrypt {
internal;
proxy_pass http://localhost:8081/;
}

location /api/encrypt {
internal;
js_content app.encrypt;
}

location /api/decrypt {
internal;
js_content app.decrypt;
}

其他:如果你追求更高效的计算

上面为了演示,我们将计算结果都进行了 Base64 编码,考虑实际生产环境中超高压力,我们一般对函数计算复杂度锱铢必较,所以可以考虑将证书硬编码到代码中,以及尽可能的去掉不必要的 Base64(只在调试模式中打开)。

最后

网络上关于 NJS 的参考资料目前还是比较少的,希望本文会成为连接你和 NJS 的纽带。

上述内容,我存放在了 GitHub 上,感兴趣的同学可以自取。

–EOF


我们有一个小小的折腾群,里面聚集了几百位喜欢折腾的小伙伴。

在不发广告的情况下,我们在里面会一起聊聊软硬件、HomeLab、编程上的一些问题,也会在群里不定期的分享一些技术沙龙的资料。

喜欢折腾的小伙伴欢迎扫码添加好友。(添加好友,请备注实名,注明来源和目的,否则不会通过审核)

关于折腾群入群的那些事


如果你觉得内容还算实用,欢迎点赞分享给你的朋友,在此谢过。


本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 署名 4.0 国际 (CC BY 4.0)

本文作者: 苏洋

创建时间: 2021年11月14日
统计字数: 16413字
阅读时间: 33分钟阅读
本文链接: soulteary.com/2021/11/14/…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

LeetCode 138 复制带随机指针的链表【c++/j

发表于 2021-11-14

「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战」

1、题目

给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。

构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。

例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。

返回复制链表的头节点。

用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个[val, random_index]表示:

  • val:一个表示 Node.val 的整数。
  • random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。

你的代码 只 接受原链表的头节点 head作为传入参数。

示例 1:

在这里插入图片描述

1
2
css复制代码输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

示例 2:

在这里插入图片描述

1
2
lua复制代码输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]

示例 3:

在这里插入图片描述

1
2
lua复制代码输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]

示例 4:

1
2
3
ini复制代码输入:head = []
输出:[]
解释:给定的链表为空(空指针),因此返回 null。

2、思路(1)

(迭代) O(n)O(n)O(n)

题目要求我们复制一个长度为 n 的链表,该链表除了每个节点有一个指针指向下一个节点外,还有一个额外的指针指向链表中的任意节点或者null,如下图所示:


如何去复制一个带随机指针的链表?

首先我们可以忽略random 指针,然后对原链表的每个节点进行复制,并追加到原节点的后面,而后复制random指针。最后我们把原链表和复制链表拆分出来,并将原链表复原。

图示过程如下:

1、在每个节点的后面加上它的复制,并将原链表和复制链表连在一起。


2、 从前往后遍历每一个原链表节点,对于有random指针的节点p,我们让它的p->next->random = p->random->next,这样我们就完成了对原链表random指针的复刻。


3、最后我们把原链表和复制链表拆分出来,并将原链表复原。


具体过程如下:

  • 1、定义一个p指针,遍历整个链表,复制每个节点,并将原链表和复制链表连在一起。
  • 2、再次遍历整个链表,执行p->next->random = p->random->next,复制random指针。
  • 3、定义虚拟头节点dummy用来指向复制链表的头节点, 将两个链表拆分并复原原链表。

时间复杂度分析: O(n)O(n)O(n),其中 nnn 是链表的长度。

空间复杂度分析: O(1)O(1)O(1)。

3、c++代码

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
c复制代码/*
// Definition for a Node.
class Node {
public:
int val;
Node* next;
Node* random;

Node(int _val) {
val = _val;
next = NULL;
random = NULL;
}
};
*/

class Solution {
public:
Node* copyRandomList(Node* head) {
for(auto p = head; p; p = p->next->next) //复制每个节点,并将原链表和复制链表连在一起。
{
auto q = new Node(p->val);
q->next = p->next;
p->next = q;
}

for(auto p = head; p; p = p->next->next) //复制random指针
{
if(p->random)
p->next->random = p->random->next;
}

//拆分两个链表,并复原原链表
auto dummy = new Node(-1), cur = dummy;
for(auto p = head; p; p = p->next)
{
auto q = p->next;
cur = cur->next = q;
p->next = q->next;
}

return dummy->next;
}
};

4、java代码

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
java复制代码/*
// Definition for a Node.
class Node {
int val;
Node next;
Node random;

public Node(int val) {
this.val = val;
this.next = null;
this.random = null;
}
}
*/

class Solution {
public Node copyRandomList(Node head) {
for(Node p = head; p != null; p = p.next.next) //复制每个节点,并将原链表和复制链表连在一起。
{
Node q = new Node(p.val);
q.next = p.next;
p.next = q;
}

for(Node p = head; p != null; p = p.next.next) //复制random指针
{
if(p.random != null)
p.next.random = p.random.next;
}

//拆分两个链表,并复原原链表
Node dummy = new Node(-1), cur = dummy;
for(Node p = head; p != null; p = p.next)
{
Node q = p.next;
cur = cur.next = q;
p.next = q.next;
}

return dummy.next;
}
}

5、思路(2)

(哈希,回溯) O(n)O(n)O(n)

使用回溯的方式遍历整个链表,让每个节点的复制相互独立。对于当前节点node,首先复制当前节点node,而后对当前节点的后继节点和随机指针指向的节点进行复制,最后让复制节点的next指针和random指针指向这两个复制的节点,即可完成边的复制。


为了防止多次遍历同一个节点,我们需要建立一个哈希表, 来记录源节点到克隆节点之间的映射关系。在回溯搜索过程中,如果当前正在搜索的节点出现在了哈希表中,就说明我们已经遍历完了整个链表,此时就可以直接从哈希表中取出复制后的节点的指针并返回。


具体过程如下:

  • 1、从head节点开始dfs遍历整个图。
  • 2、复制当前节点node,并使用哈希表hash存贮源节点到复制节点之间的映射。
  • 3、递归调用当前节点node的后继节点和随机指针指向的节点,并进行复制,然后让复制节点的next指针和random指针指向这两个复制的节点。
  • 4、最后返回已经被访问过的节点的复制节点。

时间复杂度分析: O(n)O(n)O(n),其中 nnn 是链表的长度 。
空间复杂度分析: O(n)O(n)O(n),其中 nnn 是链表的长度 。

6、c++代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
c复制代码class Solution {
public:
unordered_map<Node*, Node*> hash;

Node* copyRandomList(Node* head)
{
if (head == NULL) return NULL;
return dfs(head);
}

Node* dfs(Node* node)
{
if(node == NULL) return NULL;
//node节点已经被访问过了,直接从哈希表hash中取出对应的克隆节点返回。
if(hash.count(node)) return hash[node];
Node* clone = new Node(node->val); //复制节点
hash[node] = clone; //建立源节点到复制节点的映射
clone->next = dfs(node->next); //复制边
clone->random = dfs(node->random);
return clone;
}
};

7、java代码

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
java复制代码/*
// Definition for a Node.
class Node {
int val;
Node next;
Node random;

public Node(int val) {
this.val = val;
this.next = null;
this.random = null;
}
}
*/

class Solution {
Map<Node,Node> hash = new HashMap<>();
public Node copyRandomList(Node head) {
if(head == null) return null;
return dfs(head);
}
Node dfs(Node node)
{
if(node == null) return null;
//node节点已经被访问过了,直接从哈希表hash中取出对应的复制节点返回。
if(hash.containsKey(node)) return hash.get(node);
Node clone = new Node(node.val); //复制节点
hash.put(node,clone); //建立源节点到复制节点的映射
clone.next = dfs(node.next); //复制边
clone.random = dfs(node.random);
return clone;
}
}

原题链接: 138. 复制带随机指针的链表
在这里插入图片描述

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何实现 LRU 缓存淘汰算法 如何实现 LRU 缓存淘汰算

发表于 2021-11-14

如何实现 LRU 缓存淘汰算法

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

笔者最近在学习《数据结构与算法之美》,正好借着这个机会边练习边记录一下自己学习的知识点。不啰嗦,直接开始。

一、什么是缓存

缓存 一种提高数据读取性能的技术,有着广泛的应用,例如常见的 CPU 缓存、数据库缓存、浏览器缓存等等。

二、常见的缓存淘汰策略

缓存的大小是有限的。当缓存满了,哪些缓存数据应该被清除,哪些缓存数据应该被保留?这就应该由缓存淘汰策略来决定缓存数据的去留。常见的缓存淘汰策略有三种:

  • 先进先出策略 FIFO:先进入缓存中的数据,先被淘汰。
  • 最少使用策略 LFU:最近使用频率最少的缓存数据,先被淘汰。
  • 最近最少使用策略 LRU:最近一段时间最少被访问的数据,先被淘汰。

这里帮大家举个例子去记忆这三种淘汰策略,例如去年双十一买了一堆没用却用没有退货的东西,今年双十一又剁手买了一堆没有用的东西,房间堆满了,哪些该被清理掉?是不是我们清理的东西想法和这三种淘汰策略不谋而合了。接下来看我们今天的主角 LRU 最近最少使用策略具体实现思路吧。

三、LRU 淘汰策略实现思路

  • 缓存没有满时,新数据就放到缓存的前面。
  • 缓存已经满了,这是需要分两种情况去讨论:
    • 一是新的数据之前已经存在缓存中,此时我们需要将之前存在缓存中的数据清出缓存,再将新数据放到缓存的前面。
    • 二是新数据不在缓存中,此时我们需要先清除缓存中最后一个数据,再将新数据放到缓存的前面。

从这个思路我们可以看到,LRU 缓存淘汰策略的一个特点,需要频繁的进行数据插入删除操作。那么那种数据结构擅长进行删除和插入操作的呢?答案呼之欲出:链表。ok,接下来我们就使用链表结合实现思路转换成具体的代码。

四、LRU 淘汰策略代码实现

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
java复制代码import java.util.LinkedList;

/**
* @author xuls
* @date 2021/11/14 16:23
* LRU 缓存淘汰算法
*/
public class LRU <T>{
private LinkedList<T> list;
private int capacity; //缓存容量

public LRU() {
this(10);
}

public LRU(int capacity) {
this.capacity = capacity;
list = new LinkedList<>();
}

public void add(T data){
if (list.size() < capacity){
//缓存未满,直接添加到链表头部,表明该数据最近在使用
list.addFirst(data);
}else {
//链表之中是否包含 data
if (list.contains(data)){
//链表包含 data ,先将包含的数据移除
list.remove(data);
}else {
//链表不包含 data ,移除最后一个,因为最后一个是最近最少使用的
list.removeLast();
}
//将新数据添加到前面
list.addFirst(data);
}
}

@Override
public String toString() {
return "LRU{" +
"list=" + list +
'}';
}

public static void main(String[] args) {
LRU<String> stringLRU = new LRU<>();
for (int i = 0; i < 10; i++) {
stringLRU.add("s"+i);
}
System.out.println(stringLRU);
stringLRU.add("s1");
System.out.println(stringLRU);
stringLRU.add("s10");
System.out.println(stringLRU);
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringBoot使用Swagger 1 配置Swagg

发表于 2021-11-14

「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战」

  1. 配置Swagger

1.1 配置Swagger-ui页面的基本信息

由于没有set方法,所以只能使用构造器进行设置

不过一般需要设置的的地方很少,都是固定写法就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2).apiInfo();
}
//配置Swagger信息==>ApiInfo
public ApiInfo apiInfo(){
//作者信息
Contact contact=new Contact("杨明宇","http://blog.ymy12138.cn","1064576969@qq.com");
return new ApiInfo("Api Documentation",//标题
"Api Documentation",//详细描述
"1.0",//版本
"urn:tos",//开发者信息,可以是个网站
contact,//作者信息
"Apache 2.0",//依赖
"http://www.apache.org/licenses/LICENSE-2.0",
new ArrayList());

}
}

1.2 配置Swagger扫描接口

也就是配置扫描Controller和对应的返回值

使用链式的写法在创建Docket的时候传入其中作为参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.enable(true);
.apis(RequestHandlerSelectors.xxx())
.paths(PathSelectors.yyy());
.build();
//apis,扫描哪些
//xxx
//1. basePackage:指定要扫描的包,一般就使用这个就可以了
//2. any():扫描全部
//3. none():不扫描
//4. withClassAnnotation:扫描类上的注解,注解的反射对象
//5. withMethodAnnotation:扫描方法上的注解
//****************************************************
//paths,过滤掉哪些
//yyy
//1. ant():表示过滤哪个包
//2. any():表示过滤全部
//3. none():表示都不过滤
//**************************************
//enable,表示是否打开swagger的功能,默认true,如果修改成false,那么就无法正常使用swagger功能

问题

如何做到swagger在生产环境中使用,在发布的环境中不使用

  • 判断是不是生产环境

    1. 使用springBoot自带的设置环境的方法,创建多个配置文件application.properties
    2. 在主配置文件中指定当前的激活的环境是哪个spring.profiles.active=dev,dev就是application-xxx.properties中的xxx
    3. 在代码中获取当前的环境,再进行判断是否是生产环境
      1
      2
      3
      4
      java复制代码//在创建Docket的时候让其传入一个Environment对象,注意是springframe的包下的
      Profiles profiles=Profiles.of("dev","test");
      boolean flag=environment.acceptsProfiles(profiles);
      //这样如果是dev环境,就会返回true
  • 注入enable()

1
2
3
4
5
6
7
java复制代码return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(flag);
.select()
.apis(RequestHandlerSelectors.xxx())
.paths(PathSelectors.yyy());
.build();

这样之后,只需要在配置文件中确定当前是在哪个环境就可以改变是否会显示Swagger

  1. 配置分组

在创建Docket的时候可以传入一个.groupname("组名")来设置当前的文档是在哪个组下的

如何配置多个组?

如果有多个Docket实例就会有多个组,这样就会出现很多个组在右上角的索引中

总结

最后的config

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
java复制代码package com.ymy.swagger.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.service.Documentation;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import sun.security.acl.AllPermissionsImpl;

import java.util.ArrayList;

/**
* @Version 1.0
* @Author:ymy
* @Date:2019/8/31
* @Content:
*/

@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket docket(Environment environment){
Profiles profiles=Profiles.of("dev","test");
boolean flag=environment.acceptsProfiles(profiles);
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(flag)
.select()
.apis(RequestHandlerSelectors.basePackage("com.ymy"))
.build();
}
//配置Swagger信息==>ApiInfo
public ApiInfo apiInfo(){
//作者信息
Contact contact=new Contact("杨明宇","http://blog.ymy12138.cn","1064576969@qq.com");
return new ApiInfo("Api Documentation",//标题
"Api Documentation",//详细描述
"1.0",//版本
"urn:tos",//开发者信息,可以是个网站
contact,//作者信息
"Apache 2.0",//依赖
"http://www.apache.org/licenses/LICENSE-2.0",
new ArrayList());
}
}

这样的话,就需要设置当前的环境是在dev的环境下才可以看到swagger

如果不设置就使用下面这个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2).apiInfo();
}
//配置Swagger信息==>ApiInfo
public ApiInfo apiInfo(){
//作者信息
Contact contact=new Contact("杨明宇","http://blog.ymy12138.cn","1064576969@qq.com");
return new ApiInfo("Api Documentation",//标题
"Api Documentation",//详细描述
"1.0",//版本
"urn:tos",//开发者信息,可以是个网站
contact,//作者信息
"Apache 2.0",//依赖
"http://www.apache.org/licenses/LICENSE-2.0",
new ArrayList());

}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

5910 检查两个字符串是否几乎相等

发表于 2021-11-14

这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战

  1. 检查两个字符串是否几乎相等

如果两个字符串 word1 和 word2 中从 ‘a’ 到 ‘z’ 每一个字母出现频率之差都 不超过 3 ,那么我们称这两个字符串 word1 和 word2 几乎相等 。

给你两个长度都为 n 的字符串 word1 和 word2 ,如果 word1 和 word2 几乎相等 ,请你返回 true ,否则返回 false 。

一个字母 x 的出现 频率 指的是它在字符串中出现的次数。

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
arduino复制代码示例 1:

输入:word1 = "aaaa", word2 = "bccb"
输出:false
解释:字符串 "aaaa" 中有 4 个 'a' ,但是 "bccb" 中有 0 个 'a' 。
两者之差为 4 ,大于上限 3 。
示例 2:

输入:word1 = "abcdeef", word2 = "abaaacc"
输出:true
解释:word1 和 word2 中每个字母出现频率之差至多为 3 :
- 'a' 在 word1 中出现了 1 次,在 word2 中出现了 4 次,差为 3 。
- 'b' 在 word1 中出现了 1 次,在 word2 中出现了 1 次,差为 0 。
- 'c' 在 word1 中出现了 1 次,在 word2 中出现了 2 次,差为 1 。
- 'd' 在 word1 中出现了 1 次,在 word2 中出现了 0 次,差为 1 。
- 'e' 在 word1 中出现了 2 次,在 word2 中出现了 0 次,差为 2 。
- 'f' 在 word1 中出现了 1 次,在 word2 中出现了 0 次,差为 1 。
示例 3:

输入:word1 = "cccddabba", word2 = "babababab"
输出:true
解释:word1 和 word2 中每个字母出现频率之差至多为 3 :
- 'a' 在 word1 中出现了 2 次,在 word2 中出现了 4 次,差为 2 。
- 'b' 在 word1 中出现了 2 次,在 word2 中出现了 5 次,差为 3 。
- 'c' 在 word1 中出现了 3 次,在 word2 中出现了 0 次,差为 3 。
- 'd' 在 word1 中出现了 2 次,在 word2 中出现了 0 次,差为 2 。

提示:

  • n == word1.length == word2.length
  • 1 <= n <= 100
  • word1 和 word2 都只包含小写英文字母。

解题思路

  1. 先统计字符串word1中每个字符出现的次数
  2. 再统计字符串word1中每个字符出现的次数,减去word1字母的出现次数
  3. 检查是否存在出现次数差值大于3的字母

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cpp复制代码class Solution {
public:
bool checkAlmostEquivalent(string word1, string word2) {

int a[26]={0};
for (int i = 0; i < word1.size(); ++i) {
a[word1[i]-'a']++;
}
for (int j = 0; j < word2.size(); ++j) {
a[word2[j]-'a']--;
}
for (int i = 0; i < 26; ++i) {
if (abs(a[i])>3)
return false;
}
return true;
}
};

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Redis入门】Redis中的配置文件--redisco

发表于 2021-11-14

这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战

👉写在前边

  • 上篇我们了解了 redis中常见的数据类型和命令,接下来我们来解析一下redis中的配置文件– redis.conf 。

Redis.conf配置

Units单位

配置大小单位,开头定义了一些基本的度量单位,只支持bytes,不支持bit

大小写不敏感

INCLUDES包含

类似jsp中的include,多实例的情况可以把公用的配置文件提取出来

让外网连接

可以设置bind为对应主机的ip

  • 若想让所有ip都能访问,注释掉这一行就好了

同时protected-mode 也得设置成no

设置完这些之后,最后设置一下密码,具体可以参考之前安装的时候的设置Redis安装和运行

Port

端口号,默认 6379

tcp-backlog

设置tcp的backlog,backlog其实是一个连接队列,backlog队列总和=未完成三次握手队列+ 已经完成三次握手队列。

在高并发环境下你需要一个高backlog值来避免慢客户端连接问题。

注意Linux内核会将这个值减小到/proc/sys/net/core/somaxconn的值(128),所以需要确认增大/proc/sys/net/core/somaxconn和/proc/sys/net/ipv4/tcp_max_syn_backlog(128)两个值来达到想要的效果

timeout

一个空闲的客户端维持多少秒会关闭,0表示关闭该功能。即永不关闭。

tcp-keepalive

对访问客户端的一种心跳检测,每个n秒检测一次。

单位为秒,如果设置为0,则不会进行Keepalive检测,建议设置成60

pidfile

存放pid文件的位置,每个实例会产生一个不同的pid文件

loglevel

指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为notice

四个级别根据使用阶段来选择,生产环境选择notice 或者warning

databases 16

设定库的数量默认16,默认数据库为0,可以使用SELECT 命令在连接上指定数据库id

Limt限制

maxclients

Ø 设置redis同时可以与多少个客户端进行连接。

Ø 默认情况下为10000个客户端。

Ø 如果达到了此限制,redis则会拒绝新的连接请求,并且向这些连接请求方发出“max number of clients reached”以作回应。

maxmemory

Ø 建议必须设置,否则,将内存占满,造成服务器宕机

Ø 设置redis可以使用的内存量。一旦到达内存使用上限,redis将会试图移除内部数据,移除规则可以通过maxmemory-policy来指定。

Ø 如果redis无法根据移除规则来移除内存中的数据,或者设置了“不允许移除”,那么redis则会针对那些需要申请内存的指令返回错误信息,比如SET、LPUSH等。

Ø 但是对于无内存申请的指令,仍然会正常响应,比如GET等。如果你的redis是主redis(说明你的redis有从redis),那么在设置内存使用上限时,需要在系统中留出一些内存空间给同步队列缓存,只有在你设置的是“不移除”的情况下,才不用考虑这个因素。

maxmemory-policy

Ø volatile-lru:使用LRU算法移除key,只对设置了过期时间的键;(最近最少使用)

Ø allkeys-lru:在所有集合key中,使用LRU算法移除key

Ø volatile-random:在过期集合中移除随机的key,只对设置了过期时间的键

Ø allkeys-random:在所有集合key中,移除随机的key

Ø volatile-ttl:移除那些TTL值最小的key,即那些最近要过期的key

Ø noeviction:不进行移除。针对写操作,只是返回错误信息

maxmemory-samples

Ø 设置样本数量,LRU算法和最小TTL算法都并非是精确的算法,而是估算值,所以你可以设置样本的大小,redis默认会检查这么多个key并选择其中LRU的那个。

Ø 一般设置3到7的数字,数值越小样本越不准确,但性能消耗越小。

daemonize

Redis 默认不是以守护进程的方式运行,可以通过该配置项修改,使用 yes 启用守护进程
image.png

持久化配置

rdb

image.png

aof

image.png

设置密码

通过命令行修改(不推荐,重启就没了)

image.png

修改redis.conf

vim进入后,直接输入/requirepass

找到被注释掉的那一栏,自行设置密码

密码设置之后,当你退出再次连上redis的时候,就需要输入密码了,不然是无法操作的。

这里有两种方式输入密码,一是连接的时候直接输入密码,而是连接上之后再输入密码

连接时输入密码

-a 后边加密码

image.png

连接后输入密码

image.png

限制client(一般不用管)

image.png

💠下篇预告

  • 下篇我们将进行Springboot中的整合以及自定义RedisTemplate和工具类,之后我们将正式进入redis中的重点–持久化技术!!!

参考

  • 尚硅谷Redis6视频
  • 狂神说Redis视频

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…345346347…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%