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

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


  • 首页

  • 归档

  • 搜索

Mysql的ON DUPLICATE KEY UPDATE使

发表于 2021-03-08

mysql存在插入,不存在更新操作
insert into table_name (key1,key2,key3)VALUEs(?,?,?) ON DUPLICATE KEY
UPDATE key1 = VALUES(value1),key2=VALUES(value2),updatetime = CURRENT_TIMESTAMP;

对比直接插入
inert into table_name(key1,key2,key3)VALUES(?,?,?)

多了ON DUPLICATE KEY UPDATE key1=VALUES(value1);
建立数据表的时候需要用关键字UNIQUE指定唯一字段,

比如设置id_only为唯一字段

1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码CREATE TABLE `get_query_shop` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`shopId` varchar(100) DEFAULT NULL COMMENT '店铺Id',
`cid1Name` varchar(100) DEFAULT NULL COMMENT '一级类目名称',
`cid2Name` varchar(1000) DEFAULT NULL COMMENT '二级类目名称',
`id_only` varchar(100) DEFAULT NULL COMMENT '标记唯一订单行:',
`orderTime` varchar(100) DEFAULT NULL COMMENT '下单时间',

`createtime` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '插入时间',
`updatetime` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `id_only` (`id_only`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

需要更新那些字段就添加在ON DUPLICATE KEY UPDATE后面,可以更新一个或多个字段
例如更新orderTime和cid1Name:

1
2
3
sql复制代码insert into get_query_shop (shopId,cid1Name,cid2Name,id_only,orderTime)VALUEs
("10010","1","1-2","a","20210202") ON DUPLICATE KEY UPDATE orderTime =
VALUES("20210202"),cid1Name=VALUES("1-2"),updatetime = CURRENT_TIMESTAMP;

需要注意的是:这种插入法,数据的ID字段不会连续,并且直接inster插入会出错,因为有唯一字段,一旦出现重复数据将无法插入

如果建表的时候忘记设置,或者设置错误,可以在后续继续修改和添加

1
2
3
4
sql复制代码添加唯一字段:可以是多个,将上表shopId设置为唯一字段 
ALTER TABLE get_query_shop ADD unique(`shopId)
删除唯一字段id_only(只是删除唯一字段,字段还保留在数据表)
alter table get_query_pdd drop index `id_only`;

本文转载自: 掘金

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

亿级用户中心的设计与实践 一、服务架构 二、接口设计 三、分

发表于 2021-03-08

用户中心是互联网最为基础的核心系统,随着业务和用户的增长,势必会带来不断的挑战。如何在亿级的情况下保证系统的高可用,高性能以及高安全,本文能够给你一套实践方案。

注1:本文讨论的是微服务框架下的用户中心,不涉及授权等功能;

注2:本文所涉及的用户中心设计与vivo自身业务无关。

用户中心,顾名思义就是管理用户的地方,几乎是所有互联网公司最为核心的子系统之一。它的核心功能是登录与注册,主要功能是修改密码、换绑手机号码、获取用户信息、修改用户信息和一些延伸服务,同时还有登录之后生成Token以及校验Token的功能。下面我们从几个维度来拆解用户中心。

一、服务架构

用户中心既需要为用户提供服务,也会承担其他业务的频繁调用;既然需要为用户提供服务,它就会自带一些业务逻辑,比如用户在登录过程中需要风控或短信的校验,那么就会存在不可用的风险。而比如获取用户信息的接口,则没有那么多的依赖,可能只需要调用数据库或者缓存就可以。获取用户信息接口要求稳定,而核心的登录注册接口也需要稳定,但是当我们在接口层面加一些策略或者修改的时候,不希望因为上线问题导致整个服务不可用,而且上线后,需要对整个服务功能做全量的回归,导致资源严重浪费。

因此,基于业务特性,我们可以将用户中心拆成3个独立的微服务: 网关服务,核心服务,异步消费者服务。网关服务,提供http服务,聚合了各种业务逻辑和服务调用,比如登录时候需要校验的风控或者短信;核心服务,处理简单的业务逻辑以及数据存储,核心服务处在调用链路的终端,几乎不依赖调用其他服务,比如校验Token或者获取用户信息,他们就只依赖于redis或者数据库;而异步消费者服务,则处理并消费异步消息。下文会详细介绍。

这样的设计之后,当有新功能上线时,核心服务和异步消费服务几乎不需要重新发布,只需要发布网关服务,依赖我们核心服务的第三方非常放心,层级也非常的清晰。当然,这样做的代价就是服务的调用链路变长了。由于涉及到网关和核心服务,就需要发布两个服务,而且要做兼容性测试。

二、接口设计

用户中心的接口涉及到用户的核心信息,安全性要求高;同时,承接了较多第三方的调用,可用性要求也高。因此,对用户中心的接口做以下设计:

首先,接口可以拆分为面向Web和面向App的接口。Web接口需要做到跨域情况下的单点登录,加密、验签和token校验的方式也同App端的不一样。

其次,对核心接口做特殊处理。比如登录接口,在逻辑和链路上做了一些优化。为什么要对这些接口做特殊处理呢?假如用户不能登录,用户会非常恐慌,客诉量会立马上来。

那怎么做呢?一方面,我们将用户核心信息表做简单。用户的信息当中会包含userId、手机号码、密码、头像、昵称等字段,假如把用户的这些所有信息都保存在一张表中,那么这张表将会异常庞大,变更字段变得异常困难。因此,需要将用户表拆分,将核心的信息保存在用户表中,比如userId、username、手机号码、密码、盐值(随机生成)等;而一些如性别,头像,昵称等信息保存在用户资料表中。

另一方面,我们需要将登录的核心链路做短,短到只依赖于读库。一般情况下,用户登录后,需要记录用户登录信息,调用风控或者短信等服务。对于登录链路来说,任何一个环节出现问题都有可能导致用户无法登录,那么怎么样才能做到最短的链路呢?方法就是依赖的服务可自动降级。比如说反欺诈校验出问题了,那么它自动降级后使用它的默认策略,极端情况下只做密码校验,主库挂了之后还能到从库读取用户信息。

最后就是接口的安全性校验。对App接口我们需要做防重放和验签。验签可能大家比较熟悉,但是对防重放这个概念可能相对陌生。防重放,顾名思义就是防止请求重复发送。用户请求在特定时间段内只能请求一次。即使用户请求被攻击者挟持,在一段时间内也无法重复请求。如果攻击者想要篡改用户请求再发送,对不起,请求不会通过。得益于大数据的支持,结合终端,我们还可以把每个用户行为画像存储在系统中(或者调用第三方服务)。用户发起请求后,我们的接口会根据用户画像对用户进行诸如手机号码校验、实名认证、人脸或者活体校验。

三、分库分表

随着用户的增长,数据超过了1亿,怎么办?常见的办法就是分库分表。我们来分析一下用户中心常见的一些表结构:用户信息表,第三方登录关联表,用户事件表。从上述表中可以看出来,用户相关的数据表增长相对缓慢,因为用户增长是有天花板的。用户事件表的增长是呈指数级增长,因为每个用户登录、变更等密码及变更手机号码等操作是不限次数。

因此,首先我们可以先把用户信息表垂直切分。正如上面说的,将用户ID、密码、手机号、盐值等常见字段从用户信息表中拆分,其他用户相关的信息用单独一张表。另外,把用户事件表迁移至其他库中。相比于水平切分,垂直切分的代价相对较少,操作起来相对简单。用户核心信息表由于数据量相对较少,即使是亿级别的数据,利用数据库缓存的机制,也能够解决性能问题。

其次,我们可以利用前后台业务的特性采用不同的方式来区别对待。对于用户侧前台访问:用户通过username/mobile登录或者通过uid来查询用户信息。用户侧信息的访问通常是单条数据的查询,我们可以通过索引多次查询来解决一致性和高可用问题。对于运营侧后台访问:根据年龄、性别、登录时间段、注册时间段等来进行查询,基本上都是批量分页查询。但是由于是内部系统,查询量低,对一致性要求低。如果用户侧和运营侧的查询采用同一个数据库,那么运营侧的排序查询会导致整个库的CPU上升,查询效率下降,影响到用户侧。因此,运营侧使用的数据库可以是和用户侧同样的MySQL离线库,如果想要增加运营侧的查询效率,可以采用ES非关系型数据库。ES支持分片与复制,方便水平分割和扩展,复制保证了ES的高可用与高吞吐,同时能够满足运营侧的查询需求。

最后,如果还是要水平切分来保证系统的性能,那么我们采取什么样的切分方式呢?常见的方法有索引表法和基因法。索引表法的思路主要是UID能够直接定位到库,但是手机号码或者username是无法直接定位到库的,需要建立一个索引表来记录mobile与UID或者username与UID的映射关系的方式来解决这个问题。通常这类数据比较少,可以不用分库分表,但是相比直接查询,多了一次数据库查询的同时,在新增数据的时候还多了一次映射关系的插入,事务变大。基因法的思路是我们将username或者mobile融入到UID中。具体做法如下:、

  1. 用户注册时,根据用户的手机号码,利用函数生成N bit的基因mobile_gen,使得mobile_gen=f(mobile);
  2. 生成M bit全局唯一的id,作为用户标识;
  3. 拼接M和N,作为UID赋给用户;
  4. 根据N bit来取余来插入到特定数据库;
  5. 查找用户数据的时候,将用户UID的后N bit取余来落到最终的库中。

从上述过程中看,基因法只适用于某类经常查询的场景,比如用手机号码登录,如果用户使用username登录就比较麻烦了。因此大家以根据自己的业务场景来选择不同的方式水平切分。

四、Token之柔性降级

用户登录之后,另一个重要的事情就是Token的生成与校验。用户的Token分为两类, 一类是web端登陆生成的Token, 这个Token可以和Cookie结合, 达到单点登陆的效果,在此不细说了。另外一类就是APP端登录生成的Token。用户在我们的APP输入用户名密码之后,服务端会对用户的用户名密码进行校验,成功之后从系统配置中心获取加密算法的版本以及秘钥,并按照一定的格式排列用户ID,手机号、随机码以及过期时间,经过一系列的加密之后,生成了Token之后并将其存入Redis缓存。而Token的校验就是把用户ID和Token组合并校验是否在Redis中存在。那么假如Redis不可用了怎么办呢?这里有一个高可用和自动降级的设计。当Redis不可用的时候, 服务端会生成一个特殊格式的Token。当校验Token的时候,会对Token的格式进行一个判断。

假如判断为Redis不可用时生成的Token,那么服务端会对Token进行解密,而Token的生成是由用户ID,手机号、随机码和过期时间等数据按照特定顺序排列并加密而来的, 那么解密出来的数据中也包含了ID,手机号码,随机码和过期时间。服务端会根据获取到的数据查询数据库, 比对之后告诉用户是否登录成功。由于内存缓存redis和数据库缓存性能的差距,在redis不可用的情况下,降级有可能会导致数据库无法及时响应,因此需要在降级的方法上加入限流。

五、数据安全

数据安全对用户中心来说非常重要。敏感数据需要脱敏处理,对密码更是要做多重的加密处理。应用虽然有自己的安全策略,但如果把黑客限制在登录之前,那应用的安全性将得到大幅度的提升。互联网上用户明文数据遭到泄露的案件屡屡发生,因此各大企业对数据安全的认识也提到了前所未有的高度。而即使使用了MD5和salt的加密方式,依然可以使用彩虹表的方式来破解。那么用户中心对用户信息是怎么保存的呢?

首先,正如上文中提到的用户密码、手机号等登录信息和其他的信息分离,而且在不同的数据库中。其次,对用户设置的密码进行了黑名单校验,只要符合条件的弱密码,都会拒绝提交,因为不管使用了什么加密方式的弱密码,都极其容易破解。为什么呢?因为人的记性很差,大部分人总是最倾向于选择生日,单词等来当密码。6位纯数字可以生成100万个不同的密码,8位小写字母和数字的组合大概可以生成2.8万亿个不同的密码。一个规模为7.8万亿的密码库足以覆盖大部分用户的密码,对于不同的加密算法都可以拥有这样一个密码库,这也就是为什么大部分网站都建议用户使用8位以上数字加字母密码的原因。当然,如果一方面加了盐值,另一方面对密钥分开保管,破解难度会指数级增加。

最后,可以用bcrypt/scrypt的方式来加密。bcrypt算法是基于Blowfish块密钥算法来实现的,bcrypt内部实现了随机加盐处理,使用bcrypt之后每次加密后的密文都不一样,同时还会使用内存初始化hash过程。由于使用内存,虽然在CPU上运行很快,但是在GPU上并行运算并不快。随着新的FPGA集成了大型RAM,解决了内存密集IO的问题,但是破解难度依然不小。而scrypt算法弥补了bcrypt算法的不足,它将CPU计算与内存使用开销都指数级提升了。bcrypt和scrypt算法能够有效抵御彩虹表,但是安全性的提升带来了用户登录性能的下降。用户登录注册并不是一个高并发的接口,所以影响并不会特别大。因此在安全和性能方面需要依据业务类型和大小来做平衡,并不是所有的应用都需要使用这种加密方式来保护用户密码。

六、异步消费设计

此处的异步消费,就是上文提到的异步消费服务。用户在做完登录注册等操作后,需要记录用户的操作日志。同时,用户注册登录完毕后,下游业务需要对用户增加积分,赠送礼券等奖励操作。这些系统如果都同步依赖于用户中心,那么整个用户中心将异常庞大,链路非常冗长,也不符合业内的“大系统做小“的原则。依赖的服务不可用之后将会造成用户无法登录注册。因此,用户中心在用户操作完之后,将用户事件入库后发送至MQ,第三方业务监听用户事件。用户中心和下游业务解耦,同时用户操作事件入库后,在MQ不可用或者消息丢失的时候可做补偿处理。用户的画像数据也在很大程度上来源于此处的数据。

七、灵活多样的监控

用户中心涉及到用户的登录注册更改密码等核心功能,能否及时发现系统的问题成为关键指标,因此对业务的监控显得尤为重要。需要对用户中心重要接口的QPS、机器的内存使用量、垃圾回收的时间、服务的调用时间等做详细的监控。当某个接口的调用量下降的时候,监控会及时发出报警。除了这些监控之外,还有对数据库Binlog的写入,前端组件,以及基于ZipKin全链路调用时间的监控,实现从用户发起端到结束端的全面监控,哪怕出现一点问题,监控随时会告诉你哪里出问题了。比如运营互动推广注册量下降的时候,用户中心就会发出报警,可以及时通知业务方改正问题,挽回损失。

八、总结

本文从服务架构设计,接口设计,token降级,数据安全和监控等方面介绍了亿级用户中心的设计,当然用户中心的设计远不止这些,还会包含用户数据的分库分表,熔断限流,第三方登录等,在本文中就不一一赘述。尽管本文中设计的用户中心能够满足大部分公司的需求,但是还存在一些比较大的挑战:在鉴权服务增长的情况下,如何平滑的从用户中心剥离;监控的侵入性以及监控的粒度的完善;另外服务的安全性、可用性、性能的提升永远都没有尽头,也是我们孜孜追求的目标。在未来的日子里,希望能够通过大家的努力,使用户中心的技术体系更上一层楼。

作者:vivo 游戏技术团队

本文转载自: 掘金

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

【DB宝42】MySQL高可用架构MHA+ProxySQL实

发表于 2021-03-08

[toc]

一、MHA+ProxySQL架构

之前发过一篇MHA的文章,介绍了MHA相关的知识和功能测试,连接为:【DB宝19】在Docker中使用MySQL高可用之MHA 。今天这一篇给大家分享一下“MHA+中间件ProxySQL”来实现读写分离+负载均衡的相关知识。

我们都知道,MHA(Master High Availability Manager and tools for MySQL)目前在MySQL高可用方面是一个相对成熟的解决方案,是一套作为MySQL高可用性环境下故障切换和主从提升的高可用软件。它的架构是要求一个MySQL复制集群必须最少有3台数据库服务器,一主二从,即一台充当Master,一台充当备用Master,另一台充当从库。但是,如果不连接任何外部的数据库中间件,那么就会导致所有的业务压力流向主库,从而造成主库压力过大,而2个从库除了本身的IO和SQL线程外,无任何业务压力,会严重造成资源的浪费。因此,我们可以把MHA和ProxySQL结合使用来实现读写分离和负载均衡。所有的业务通过中间件ProxySQL后,会被分配到不同的MySQL机器上。从而,前端的写操作会流向主库,而读操作会被负载均衡的转发到2个从库上。

MHA+ProxySQL架构如下图所示:

二、快速搭建MHA环境

2.1 下载MHA镜像

  • 小麦苗的Docker Hub的地址:hub.docker.com/u/lhrbest
1
2
3
4
5
6
7
8
9
10
11
sql复制代码-- 下载镜像
docker pull registry.cn-hangzhou.aliyuncs.com/lhrbest/mha-lhr-master1-ip131
docker pull registry.cn-hangzhou.aliyuncs.com/lhrbest/mha-lhr-slave1-ip132
docker pull registry.cn-hangzhou.aliyuncs.com/lhrbest/mha-lhr-slave2-ip133
docker pull registry.cn-hangzhou.aliyuncs.com/lhrbest/mha-lhr-monitor-ip134

-- 重命名镜像
docker tag registry.cn-hangzhou.aliyuncs.com/lhrbest/mha-lhr-master1-ip131 lhrbest/mha-lhr-master1-ip131
docker tag registry.cn-hangzhou.aliyuncs.com/lhrbest/mha-lhr-slave1-ip132 lhrbest/mha-lhr-slave1-ip132
docker tag registry.cn-hangzhou.aliyuncs.com/lhrbest/mha-lhr-slave2-ip133 lhrbest/mha-lhr-slave2-ip133
docker tag registry.cn-hangzhou.aliyuncs.com/lhrbest/mha-lhr-monitor-ip134 lhrbest/mha-lhr-monitor-ip134

一共4个镜像,3个MHA Node,一个MHA Manager,压缩包大概3G,下载完成后:

1
2
3
4
5
sql复制代码[root@lhrdocker ~]# docker images | grep mha
registry.cn-hangzhou.aliyuncs.com/lhrbest/mha-lhr-monitor-ip134 latest 7d29597dc997 14 hours ago 1.53GB
registry.cn-hangzhou.aliyuncs.com/lhrbest/mha-lhr-slave2-ip133 latest d3717794e93a 40 hours ago 4.56GB
registry.cn-hangzhou.aliyuncs.com/lhrbest/mha-lhr-slave1-ip132 latest f62ee813e487 40 hours ago 4.56GB
registry.cn-hangzhou.aliyuncs.com/lhrbest/mha-lhr-master1-ip131 latest ae7be48d83dc 40 hours ago 4.56GB

2.2 编辑yml文件,创建MHA相关容器

编辑yml文件,使用docker-compose来创建MHA相关容器,注意docker-compose.yml文件的格式,对空格、缩进、对齐都有严格要求:

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
sql复制代码# 创建存放yml文件的路径
mkdir -p /root/mha

# 编辑文件/root/mha/docker-compose.yml
cat > /root/mha/docker-compose.yml <<"EOF"
version: '3.8'

services:
MHA-LHR-Master1-ip131:
container_name: "MHA-LHR-Master1-ip131"
restart: "always"
hostname: MHA-LHR-Master1-ip131
privileged: true
image: lhrbest/mha-lhr-master1-ip131
ports:
- "33131:3306"
- "2201:22"
networks:
mhalhr:
ipv4_address: 192.168.68.131

MHA-LHR-Slave1-ip132:
container_name: "MHA-LHR-Slave1-ip132"
restart: "always"
hostname: MHA-LHR-Slave1-ip132
privileged: true
image: lhrbest/mha-lhr-slave1-ip132
ports:
- "33132:3306"
- "2202:22"
networks:
mhalhr:
ipv4_address: 192.168.68.132

MHA-LHR-Slave2-ip133:
container_name: "MHA-LHR-Slave2-ip133"
restart: "always"
hostname: MHA-LHR-Slave2-ip133
privileged: true
image: lhrbest/mha-lhr-slave2-ip133
ports:
- "33133:3306"
- "2203:22"
networks:
mhalhr:
ipv4_address: 192.168.68.133

MHA-LHR-Monitor-ip134:
container_name: "MHA-LHR-Monitor-ip134"
restart: "always"
hostname: MHA-LHR-Monitor-ip134
privileged: true
image: lhrbest/mha-lhr-monitor-ip134
ports:
- "33134:3306"
- "2204:22"
networks:
mhalhr:
ipv4_address: 192.168.68.134

networks:
mhalhr:
name: mhalhr
ipam:
config:
- subnet: "192.168.68.0/16"

EOF

2.3 安装docker-compose软件(若已安装,可忽略)

  • 安装 Docker Compose官方文档:docs.docker.com/compose/
  • 编辑docker-compose.yml文件官方文档:docs.docker.com/compose/com…
1
2
3
4
5
6
7
8
sql复制代码[root@lhrdocker ~]# curl --insecure -L https://github.com/docker/compose/releases/download/1.28.4/docker-compose-Linux-x86_64 -o /usr/local/bin/docker-compose
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 638 100 638 0 0 530 0 0:00:01 0:00:01 --:--:-- 531
100 11.6M 100 11.6M 0 0 1994k 0 0:00:06 0:00:06 --:--:-- 2943k
[root@lhrdocker ~]# chmod +x /usr/local/bin/docker-compose
[root@lhrdocker ~]# docker-compose -v
docker-compose version 1.28.4, build cabd5cfb

2.4 创建MHA容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sql复制代码# 启动mha环境的容器,一定要进入文件夹/root/mha/后再操作
-- docker rm -f MHA-LHR-Master1-ip131 MHA-LHR-Slave1-ip132 MHA-LHR-Slave2-ip133 MHA-LHR-Monitor-ip134
[root@lhrdocker ~]# cd /root/mha/
[root@lhrdocker mha]#
[root@lhrdocker mha]# docker-compose up -d
Creating network "mhalhr" with the default driver
Creating MHA-LHR-Monitor-ip134 ... done
Creating MHA-LHR-Slave2-ip133 ... done
Creating MHA-LHR-Master1-ip131 ... done
Creating MHA-LHR-Slave1-ip132 ... done
[root@docker35 ~]# docker ps | grep "mha\|COMMAND"
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2978361198b7 lhrbest/mha-lhr-master1-ip131 "/usr/sbin/init" 2 minutes ago Up 2 minutes 16500-16599/tcp, 0.0.0.0:2201->22/tcp, 0.0.0.0:33131->3306/tcp MHA-LHR-Master1-ip131
a64e2e86589c lhrbest/mha-lhr-slave1-ip132 "/usr/sbin/init" 2 minutes ago Up 2 minutes 16500-16599/tcp, 0.0.0.0:2202->22/tcp, 0.0.0.0:33132->3306/tcp MHA-LHR-Slave1-ip132
d7d6ce34800b lhrbest/mha-lhr-monitor-ip134 "/usr/sbin/init" 2 minutes ago Up 2 minutes 0.0.0.0:2204->22/tcp, 0.0.0.0:33134->3306/tcp MHA-LHR-Monitor-ip134
dacd22edb2f8 lhrbest/mha-lhr-slave2-ip133 "/usr/sbin/init" 2 minutes ago Up 2 minutes 16500-16599/tcp, 0.0.0.0:2203->22/tcp, 0.0.0.0:33133->3306/tcp MHA-LHR-Slave2-ip133

2.5 主库131添加VIP

1
2
3
4
5
6
7
8
9
sh复制代码# 进入主库131
docker exec -it MHA-LHR-Master1-ip131 bash

# 添加VIP135
/sbin/ifconfig eth0:1 192.168.68.135/24
ifconfig

# 如果删除的话
ip addr del 192.168.68.135/24 dev eth1

添加完成后:

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
sh复制代码[root@MHA-LHR-Master1-ip131 /]# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.68.131 netmask 255.255.0.0 broadcast 192.168.255.255
ether 02:42:c0:a8:44:83 txqueuelen 0 (Ethernet)
RX packets 220 bytes 15883 (15.5 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 189 bytes 17524 (17.1 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

eth0:1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.68.135 netmask 255.255.255.0 broadcast 192.168.68.255
ether 02:42:c0:a8:44:83 txqueuelen 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
loop txqueuelen 1000 (Local Loopback)
RX packets 5 bytes 400 (400.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 5 bytes 400 (400.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

# 管理节点已经可以ping通VIP了
[root@MHA-LHR-Monitor-ip134 /]# ping 192.168.68.135
PING 192.168.68.135 (192.168.68.135) 56(84) bytes of data.
64 bytes from 192.168.68.135: icmp_seq=1 ttl=64 time=0.172 ms
64 bytes from 192.168.68.135: icmp_seq=2 ttl=64 time=0.076 ms
^C
--- 192.168.68.135 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1000ms
rtt min/avg/max/mdev = 0.076/0.124/0.172/0.048 ms

到这一步就可以验证主从复制是否正确,若正确,则可以直接测试MHA了。

1
2
3
4
5
6
7
8
9
10
sql复制代码mysql -uroot -plhr -h192.168.68.131 -P3306
show slave hosts;
mysql> show slave hosts;
+-----------+----------------+------+-----------+--------------------------------------+
| Server_id | Host | Port | Master_id | Slave_UUID |
+-----------+----------------+------+-----------+--------------------------------------+
| 573306133 | 192.168.68.133 | 3306 | 573306131 | d391ce7e-aec3-11ea-94cd-0242c0a84485 |
| 573306132 | 192.168.68.132 | 3306 | 573306131 | d24a77d1-aec3-11ea-9399-0242c0a84484 |
+-----------+----------------+------+-----------+--------------------------------------+
2 rows in set (0.00 sec)

三、配置ProxySQL环境

3.1 申请ProxySQL主机并安装ProxySQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sql复制代码docker rm -f MHA-LHR-ProxySQL-ip136
docker run -d --name MHA-LHR-ProxySQL-ip136 -h MHA-LHR-ProxySQL-ip136 \
-v /sys/fs/cgroup:/sys/fs/cgroup \
--network mhalhr --ip 192.168.68.136 \
-p 26032:6032 -p 26033:6033 -p 26080:6080 \
--privileged=true lhrbest/lhrcentos76:8.0 \
/usr/sbin/init

docker network connect bridge MHA-LHR-ProxySQL-ip136
docker restart MHA-LHR-ProxySQL-ip136

docker cp proxysql2-2.0.15-1.1.el7.x86_64.rpm MHA-LHR-ProxySQL-ip136:/
docker exec -it MHA-LHR-ProxySQL-ip136 bash
rpm -ivh proxysql2-2.0.15-1.1.el7.x86_64.rpm


systemctl start proxysql
systemctl status proxysql

3.2 添加远程登录用户

1
2
3
4
5
6
7
8
9
10
sql复制代码-- 添加远程登录用户
mysql -uadmin -padmin -h127.0.0.1 -P6032
select @@admin-admin_credentials;
set admin-admin_credentials='admin:admin;root:lhr';
select @@admin-admin_credentials;
load admin variables to runtime;
save admin variables to disk;

-- 远程登录
mysql -uroot -plhr -h192.168.66.35 -P26032

执行过程:

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
sql复制代码-- ProxySQL本地登录
[root@MHA-LHR-ProxySQL-ip136 /]# mysql -uadmin -padmin -h127.0.0.1 -P6032
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 162
Server version: 5.5.30 (ProxySQL Admin Module)

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> select @@admin-admin_credentials;
+---------------------------+
| @@admin-admin_credentials |
+---------------------------+
| admin:admin;lhr:lhr |
+---------------------------+
1 row in set (0.05 sec)

mysql> set admin-admin_credentials='admin:admin;root:lhr';
Query OK, 1 row affected (0.00 sec)

mysql> select @@admin-admin_credentials;
+---------------------------+
| @@admin-admin_credentials |
+---------------------------+
| admin:admin;root:lhr |
+---------------------------+
1 row in set (0.00 sec)

mysql> load admin variables to runtime;
Query OK, 0 rows affected (0.00 sec)

mysql> save admin variables to disk;
Query OK, 35 rows affected (0.13 sec)

mysql>

-- 远程登录
C:\Users\lhrxxt>mysql -uroot -plhr -h192.168.66.35 -P26032
mysql: [Warning] Using a password on the command line interface can be insecure.
ERROR 1045 (28000): ProxySQL Error: Access denied for user 'root'@'172.17.0.1' (using password: YES)

C:\Users\lhrxxt>mysql -uroot -plhr -h192.168.66.35 -P26032
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 163
Server version: 5.5.30 (ProxySQL Admin Module)

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [(none)]> show databases;
+-----+---------------+-------------------------------------+
| seq | name | file |
+-----+---------------+-------------------------------------+
| 0 | main | |
| 2 | disk | /var/lib/proxysql/proxysql.db |
| 3 | stats | |
| 4 | monitor | |
| 5 | stats_history | /var/lib/proxysql/proxysql_stats.db |
+-----+---------------+-------------------------------------+
5 rows in set (0.05 sec)

3.3 开启ProxySQL的web监控功能

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码-- 开启web监控功能
SET admin-web_enabled='true';
LOAD ADMIN VARIABLES TO RUNTIME;
SAVE ADMIN VARIABLES TO DISK;
select * from global_variables where variable_name LIKE 'admin-web_enabled';
select @@admin-web_enabled;

lsof -i:6080

-- 浏览器访问
https://192.168.66.35:26080
用户名和密码:stats:stats

3.4 配置被监控的数据库

3.4.1 向ProxySQL插入被监控数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sql复制代码-- 1、向ProxySQL插入被监控数据库
select * from mysql_servers;
insert into main.mysql_servers(hostgroup_id,hostname,port) values(10,'192.168.68.131',3306);
insert into main.mysql_servers(hostgroup_id,hostname,port) values(10,'192.168.68.132',3306);
insert into main.mysql_servers(hostgroup_id,hostname,port) values(10,'192.168.68.133',3306);
load mysql servers to runtime;
save mysql servers to disk;
select * from mysql_servers;
MySQL [(none)]> select * from mysql_servers;
+--------------+----------------+------+-----------+--------+--------+-------------+-----------------+---------------------+---------+----------------+---------+
| hostgroup_id | hostname | port | gtid_port | status | weight | compression | max_connections | max_replication_lag | use_ssl | max_latency_ms | comment |
+--------------+----------------+------+-----------+--------+--------+-------------+-----------------+---------------------+---------+----------------+---------+
| 10 | 192.168.68.131 | 3306 | 0 | ONLINE | 1 | 0 | 1000 | 0 | 0 | 0 | |
| 10 | 192.168.68.132 | 3306 | 0 | ONLINE | 1 | 0 | 1000 | 0 | 0 | 0 | |
| 10 | 192.168.68.133 | 3306 | 0 | ONLINE | 1 | 0 | 1000 | 0 | 0 | 0 | |
+--------------+----------------+------+-----------+--------+--------+-------------+-----------------+---------------------+---------+----------------+---------+
3 rows in set (0.07 sec)

3.4.2 在所有被监控MySQL服务器上创建监控帐户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sql复制代码-- 2、在所有被监控MySQL服务器上创建帐户,注意:新版本中,这里的密码必须为monitor,可参考配置文件/etc/proxysql.cnf
mysql -uroot -plhr -h192.168.66.35 -P33131
create user 'monitor'@'%' IDENTIFIED BY 'monitor';
GRANT all privileges ON *.* TO 'monitor'@'%' with grant option;
select user,host from mysql.user;

mysql> select user,host from mysql.user;
+---------------+--------------+
| user | host |
+---------------+--------------+
| mha | % |
| monitor | % |
| repl | % |
| root | % |
| mysql.session | localhost |
| mysql.sys | localhost |
| root | localhost |
+---------------+--------------+
7 rows in set (0.00 sec)

3.4.3 在所有被监控MySQL服务器上创建对外访问账户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sql复制代码-- 3、 在所有被监控MySQL服务器上创建对外访问账户:
create user 'wr'@'%' IDENTIFIED BY 'lhr';
GRANT all privileges ON *.* TO 'wr'@'%' with grant option;

-- 配置到ProxySQL中
insert into mysql_users(username,password,default_hostgroup) values('wr','lhr',10);
update mysql_users set transaction_persistent=1 where username='wr';
load mysql users to runtime;
save mysql users to disk;
select * from mysql_users;

MySQL [(none)]> select * from mysql_users;
+----------+----------+--------+---------+-------------------+----------------+---------------+------------------------+--------------+---------+----------+-----------------+---------+
| username | password | active | use_ssl | default_hostgroup | default_schema | schema_locked | transaction_persistent | fast_forward | backend | frontend | max_connections | comment |
+----------+----------+--------+---------+-------------------+----------------+---------------+------------------------+--------------+---------+----------+-----------------+---------+
| wr | lhr | 1 | 0 | 10 | NULL | 0 | 1 | 0 | 1 | 1 | 10000 | |
+----------+----------+--------+---------+-------------------+----------------+---------------+------------------------+--------------+---------+----------+-----------------+---------+
1 row in set (0.05 sec)

3.4.4 配置监控

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
sql复制代码-- 4、在ProxySQL端执行下列SQL语句:
set mysql-monitor_username='monitor';
set mysql-monitor_password='monitor';
load mysql servers to runtime;
save mysql servers to disk;
select * from global_variables where variable_name in('mysql-monitor_username','mysql-monitor_password');
+------------------------+----------------+
| variable_name | variable_value |
+------------------------+----------------+
| mysql-monitor_password | monitor |
| mysql-monitor_username | monitor |
+------------------------+----------------+
2 rows in set (0.05 sec)


-- 检查连接到MySQL的日志
select * from monitor.mysql_server_ping_log order by time_start_us desc limit 6;
select * from monitor.mysql_server_connect_log order by time_start_us desc limit 6;
MySQL [(none)]> select * from monitor.mysql_server_ping_log order by time_start_us desc limit 6;
+----------------+------+------------------+----------------------+------------+
| hostname | port | time_start_us | ping_success_time_us | ping_error |
+----------------+------+------------------+----------------------+------------+
| 192.168.68.132 | 3306 | 1614050308827202 | 252 | NULL |
| 192.168.68.133 | 3306 | 1614050308716530 | 370 | NULL |
| 192.168.68.131 | 3306 | 1614050308605853 | 542 | NULL |
| 192.168.68.131 | 3306 | 1614050298778908 | 334 | NULL |
| 192.168.68.133 | 3306 | 1614050298690947 | 297 | NULL |
| 192.168.68.132 | 3306 | 1614050298605725 | 344 | NULL |
+----------------+------+------------------+----------------------+------------+
6 rows in set (0.06 sec)

MySQL [(none)]> select * from monitor.mysql_server_connect_log order by time_start_us desc limit 6;
+----------------+------+------------------+-------------------------+---------------+
| hostname | port | time_start_us | connect_success_time_us | connect_error |
+----------------+------+------------------+-------------------------+---------------+
| 192.168.68.131 | 3306 | 1614050285481316 | 1173 | NULL |
| 192.168.68.133 | 3306 | 1614050284894846 | 1008 | NULL |
| 192.168.68.132 | 3306 | 1614050284309124 | 970 | NULL |
| 192.168.68.131 | 3306 | 1614050225194575 | 1108 | NULL |
| 192.168.68.133 | 3306 | 1614050224751771 | 987 | NULL |
| 192.168.68.132 | 3306 | 1614050224309026 | 1294 | NULL |
+----------------+------+------------------+-------------------------+---------------+
6 rows in set (0.05 sec)

3.4.5 配置MySQL主机组

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
sql复制代码-- 5、实验使用10作为写入组,20作为读取组。
show create table mysql_replication_hostgroups\G;
writer_hostgroup 写入组的编号
reader_hostgroup 读取组的编号


-- 注意:需要配置从库的read_only=1
show variables like 'read_only';
set global read_only=1;

insert into mysql_replication_hostgroups(writer_hostgroup,reader_hostgroup,comment) values(10,20,'proxy');
load mysql servers to runtime;
save mysql servers to disk;
select * from mysql_replication_hostgroups;
select * from mysql_server_read_only_log order by time_start_us desc limit 3;
select * from mysql_servers;

MySQL [(none)]> select * from mysql_replication_hostgroups;
+------------------+------------------+------------+---------+
| writer_hostgroup | reader_hostgroup | check_type | comment |
+------------------+------------------+------------+---------+
| 10 | 20 | read_only | proxy |
+------------------+------------------+------------+---------+
1 row in set (0.05 sec)

MySQL [(none)]> select * from mysql_server_read_only_log order by time_start_us desc limit 3;
+----------------+------+------------------+-----------------+-----------+-------+
| hostname | port | time_start_us | success_time_us | read_only | error |
+----------------+------+------------------+-----------------+-----------+-------+
| 192.168.68.133 | 3306 | 1614050367153351 | 611 | 1 | NULL |
| 192.168.68.131 | 3306 | 1614050367136396 | 490 | 0 | NULL |
| 192.168.68.132 | 3306 | 1614050367119511 | 531 | 1 | NULL |
+----------------+------+------------------+-----------------+-----------+-------+
3 rows in set (0.05 sec)

MySQL [(none)]> select * from mysql_servers;
+--------------+----------------+------+-----------+--------+--------+-------------+-----------------+---------------------+---------+----------------+---------+
| hostgroup_id | hostname | port | gtid_port | status | weight | compression | max_connections | max_replication_lag | use_ssl | max_latency_ms | comment |
+--------------+----------------+------+-----------+--------+--------+-------------+-----------------+---------------------+---------+----------------+---------+
| 10 | 192.168.68.131 | 3306 | 0 | ONLINE | 1 | 0 | 1000 | 0 | 0 | 0 | |
| 20 | 192.168.68.132 | 3306 | 0 | ONLINE | 1 | 0 | 1000 | 0 | 0 | 0 | |
| 20 | 192.168.68.133 | 3306 | 0 | ONLINE | 1 | 0 | 1000 | 0 | 0 | 0 | |
+--------------+----------------+------+-----------+--------+--------+-------------+-----------------+---------------------+---------+----------------+---------+
3 rows in set (0.05 sec)

注意,此时mysql_servers表中的hostgroup_id值已发生变化。

3.4.6 配置读写分离策略

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
sql复制代码-- 6、配置读写分离策略
insert into mysql_query_rules(active,match_pattern,destination_hostgroup,apply) values(1,'^SELECT.*FOR UPDATE$',10,1);
insert into mysql_query_rules(active,match_pattern,destination_hostgroup,apply) values(1,'^SELECT',20,1);
LOAD MYSQL QUERY RULES TO RUNTIME;
SAVE MYSQL QUERY RULES TO DISK;

-- 配置查询select的请求转发到hostgroup_id=2组上(读组)
-- 针对select * from table_name for update这样的修改语句,我们是需要将请求转到写组,也就是hostgroup_id=1
-- 对于其它没有被规则匹配的请求全部转发到默认的组(mysql_users表中default_hostgroup)
select * from mysql_query_rules;
select username,password,default_hostgroup from mysql_users;
MySQL [(none)]> select * from mysql_query_rules;
+---------+--------+----------+------------+--------+-------------+------------+------------+--------+--------------+----------------------+----------------------+--------------+---------+-----------------+-----------------------+-----------+--------------------+---------------+-----------+---------+---------+-------+-------------------+----------------+------------------+-----------+--------+-------------+-----------+---------------------+-----+-------+---------+
| rule_id | active | username | schemaname | flagIN | client_addr | proxy_addr | proxy_port | digest | match_digest | match_pattern | negate_match_pattern | re_modifiers | flagOUT | replace_pattern | destination_hostgroup | cache_ttl | cache_empty_result | cache_timeout | reconnect | timeout | retries | delay | next_query_flagIN | mirror_flagOUT | mirror_hostgroup | error_msg | OK_msg | sticky_conn | multiplex | gtid_from_hostgroup | log | apply | comment |
+---------+--------+----------+------------+--------+-------------+------------+------------+--------+--------------+----------------------+----------------------+--------------+---------+-----------------+-----------------------+-----------+--------------------+---------------+-----------+---------+---------+-------+-------------------+----------------+------------------+-----------+--------+-------------+-----------+---------------------+-----+-------+---------+
| 1 | 1 | NULL | NULL | 0 | NULL | NULL | NULL | NULL | NULL | ^SELECT.*FOR UPDATE$ | 0 | CASELESS | NULL | NULL | 10 | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | 1 | NULL |
| 2 | 1 | NULL | NULL | 0 | NULL | NULL | NULL | NULL | NULL | ^SELECT | 0 | CASELESS | NULL | NULL | 20 | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | 1 | NULL |
+---------+--------+----------+------------+--------+-------------+------------+------------+--------+--------------+----------------------+----------------------+--------------+---------+-----------------+-----------------------+-----------+--------------------+---------------+-----------+---------+---------+-------+-------------------+----------------+------------------+-----------+--------+-------------+-----------+---------------------+-----+-------+---------+
2 rows in set (0.05 sec)

MySQL [(none)]> select username,password,default_hostgroup from mysql_users;
+----------+----------+-------------------+
| username | password | default_hostgroup |
+----------+----------+-------------------+
| wr | lhr | 10 |
+----------+----------+-------------------+
1 row in set (0.05 sec)

至此,ProxySQL读写分离和负载均衡已配置完成,接下来我们进行测试。

About Me


● 本文作者:小麦苗,部分内容整理自网络,若有侵权请联系小麦苗删除

● 本文在个人微 信公众号(DB宝)上有同步更新

● QQ群号: 230161599 、618766405,微信群私聊

● 个人QQ号(646634621),微 信号(db_bao),注明添加缘由

● 于 2021年3月 在西安完成

● 最新修改时间:2021年3月

● 版权所有,欢迎分享本文,转载请保留出处


●小麦苗的微店: weidian.com/?userid=793…

●小麦苗出版的数据库类丛书: blog.itpub.net/26736162/vi…

●小麦苗OCP、OCM、高可用、DBA学习班(Oracle、MySQL、NoSQL): blog.itpub.net/26736162/vi…

●数据库笔试面试题库及解答: mp.weixin.qq.com/s/Vm5PqNcDc…


使用微信客户端扫描下面的二维码来关注小麦苗的微信公众号(DB宝)及QQ群(DBA宝典)、添加小麦苗微信, 学习最实用的数据库技术。

本文转载自: 掘金

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

我有点不喜欢分布式中的TCC模式了,求面试官别再问了 前言

发表于 2021-03-07

前言

分布式事务的解决方案中,TCC是比较经典的模式,使用2阶段提交的思想来实现分布式事务的最终一致。但最近我有点不喜欢TCC模式了。

微信公众号「程序员jinjunzhu」,作者jinjunzhu 。

TCC回顾

TCC到底是什么呢?

以经典的电商系统来说,客户购买一件商品,系统需要3个服务来协作完成。订单服务增加订单,库存服务扣减库存,账户服务扣减金额。如下图:

我有点不喜欢分布式中的TCC模式了,求面试官别再问了

如果我们用上图的方式,每个服务各自提交事务,很有可能会出现数据不一致的情况。因为3个服务使用不同数据库,并不是一个原子操作,比如订单服务提交成功而账户服务失败了,这样数据就不一致了。

TCC的思想是使用2阶段提交,try阶段首先尝试各个服务预留资源,如果预留成功则进入commit阶段提交事务,如果有一个服务预留失败,那就进入cancel阶段取消事务。这需要加入一个协调节点来对3个服务下发命令并且获取每个服务的分支事务执行结果。try阶段用下图表示:

我有点不喜欢分布式中的TCC模式了,求面试官别再问了

try阶段如果各个服务预留资源成功,协调节点就会对各服务下发commit命令,如下图:

我有点不喜欢分布式中的TCC模式了,求面试官别再问了

所有服务commit成功后,整个事务完成。

代码实现

协调节点需要给每个分布式事务提供一个全局事务id,叫做xid,用来跟每个服务的本地事务绑定。我们以账户服务为例,来看一下try/commit/cancel这3个阶段的代码:

我有点不喜欢分布式中的TCC模式了,求面试官别再问了

我有点不喜欢分布式中的TCC模式了,求面试官别再问了

这段代码使用了jdbc来处理本地事务,try阶段我们获取了connection并且保存在connectionMap,key是xid,这样在commit/cancel阶段,从connectionMap中取出connection来commit/rollback。

存在问题

上面TCC模式的代码实现有问题吗?

服务集群

如下图,如果订单服务集群部署在3个机器上,try请求发送到订单服务1,而commit请求发到订单服务2上,订单服务2的connectionMap怎么可能有xid=123的这个值呢?订单服务本地事务不能提交了。

我有点不喜欢分布式中的TCC模式了,求面试官别再问了

所以如果真要用保持connection的方式来提交事务,协调节点就需要保证同一个xid对应的try/commit/cancel请求到同一个机器上。

解决方案肯定有,改造注册中心,或者协调节点自己维护服务列表。前者让注册中心耦合了业务代码,后者相当于废弃了注册中心。

空提交

注册中心和协调节点的改造都需要很大的工作量,有没有别的方法呢?我们做一个改进,这里orm框架使用mybatis,代码如下:

我有点不喜欢分布式中的TCC模式了,求面试官别再问了

try阶段要预留资源,这段代码如果预留资源成功,其实已经提交分支事务了,commit阶段只是一个空提交,没有实际作用了。

还有一种方式就是try阶段直接返回true,到commit阶段真正提交事务。

但是这两种方式都违背了TCC的思想。

幂等

如果协调节点设置了超时重试,发生了下图的情况,订单服务1执行完try方法后发生故障,协调节点收不到成功回复必定会进行重试,这样订单服务就会重复执行try方法。

我有点不喜欢分布式中的TCC模式了,求面试官别再问了

为了规避这个问题,try/confirm/cancel方法都必须加入幂等逻辑,记录全局事务xid对应本地事务的执行状态。

空回滚

使用框架来实现TCC模式时,会有一种空回滚的情况。

我有点不喜欢分布式中的TCC模式了,求面试官别再问了

如上图,因为订单服务1节点故障,try方法失败,但是全局事务已经开启,框架必须要把这个全局事务推向结束状态,这样就不得不调用订单服务cancel方法进行回滚,结果订单服务空跑了一次cancel方法。

解决这个问题,try阶段需要记录xid对应的分支事务执行状态,cancel阶段根据这个记录来进行判断。

悬挂

上面讲了seata的使用过程中会发生空回滚,如果发生了空回滚,执行了cancel方法后全局事务结束了,但是因为网络问题,订单服务又收到了try请求,执行try方法后预留资源成功,这些资源却不能释放了。

解决这个问题的方法就是在cancel方法中记录xid对应的分支事务执行状态,try阶段执行的时候先判断分支事务是否已经回滚。

代码侵入高

TCC的try/commit/cancel,对业务代码都有侵入,如果再考虑幂等、空回滚、悬挂等,代码侵入会更高。

总结

TCC是分布式事务中非常经典的模式,但即使借助框架实现,代码实现也比较复杂。

实际使用时需要考虑服务集群、空提交、幂等、空回滚、悬挂等问题。

对业务代码侵入性很高。

本文转载自: 掘金

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

老板下死命令:必须将20M文件从30秒压缩到1秒,我是如何做

发表于 2021-03-07

压缩20M文件从30秒到1秒的优化过程

有一个需求需要将前端传过来的10张照片,然后后端进行处理以后压缩成一个压缩包通过网络流传输出去。之前没有接触过用Java压缩文件的,所以就直接上网找了一个例子改了一下用了,改完以后也能使用,但是随着前端所传图片的大小越来越大的时候,耗费的时间也在急剧增加,最后测了一下压缩20M的文件竟然需要30秒的时间。压缩文件的代码如下。
在这里插入图片描述

这里找了一张2M大小的图片,并且循环十次进行测试。打印的结果如下,时间大概是30秒。
在这里插入图片描述

第一次优化过程-从30秒到2秒

进行优化首先想到的是利用缓冲区BufferInputStream。在FileInputStream中read()方法每次只读取一个字节。源码中也有说明。
在这里插入图片描述

这是一个调用本地方法与原生操作系统进行交互,从磁盘中读取数据。每读取一个字节的数据就调用一次本地方法与操作系统交互,是非常耗时的。例如我们现在有30000个字节的数据,如果使用FileInputStream那么就需要调用30000次的本地方法来获取这些数据,而如果使用缓冲区的话(这里假设初始的缓冲区大小足够放下30000字节的数据)那么只需要调用一次就行。因为缓冲区在第一次调用read()方法的时候会直接从磁盘中将数据直接读取到内存中。随后再一个字节一个字节的慢慢返回。

BufferedInputStream内部封装了一个byte数组用于存放数据,默认大小是8192

优化过后的代码如下
在这里插入图片描述

输出
在这里插入图片描述

可以看到相比较于第一次使用FileInputStream效率已经提升了许多了
第二次优化过程-从2秒到1秒

使用缓冲区buffer的话已经是满足了我的需求了,但是秉着学以致用的想法,就想着用NIO中知识进行优化一下。

使用Channel

为什么要用Channel呢?因为在NIO中新出了Channel和ByteBuffer。正是因为它们的结构更加符合操作系统执行I/O的方式,所以其速度相比较于传统IO而言速度有了显著的提高。Channel就像一个包含着煤矿的矿藏,而ByteBuffer则是派送到矿藏的卡车。也就是说我们与数据的交互都是与ByteBuffer的交互。

在NIO中能够产生FileChannel的有三个类。分别是FileInputStream、FileOutputStream、以及既能读又能写的RandomAccessFile。

源码如下
在这里插入图片描述

我们可以看到这里并没有使用ByteBuffer进行数据传输,而是使用了transferTo的方法。这个方法是将两个通道进行直连。
在这里插入图片描述

这是源码上的描述文字,大概意思就是使用transferTo的效率比循环一个Channel读取出来然后再循环写入另一个Channel好。操作系统能够直接传输字节从文件系统缓存到目标的Channel中,而不需要实际的copy阶段。

copy阶段就是从内核空间转到用户空间的一个过程
可以看到速度相比较使用缓冲区已经有了一些的提高。
在这里插入图片描述

内核空间和用户空间

那么为什么从内核空间转向用户空间这段过程会慢呢?首先我们需了解的是什么是内核空间和用户空间。在常用的操作系统中为了保护系统中的核心资源,于是将系统设计为四个区域,越往里权限越大,所以Ring0被称之为内核空间,用来访问一些关键性的资源。Ring3被称之为用户空间。

在这里插入图片描述

用户态、内核态:线程处于内核空间称之为内核态,线程处于用户空间属于用户态

那么我们如果此时应用程序(应用程序是都属于用户态的)需要访问核心资源怎么办呢?那就需要调用内核中所暴露出的接口用以调用,称之为系统调用。例如此时我们应用程序需要访问磁盘上的文件。此时应用程序就会调用系统调用的接口open方法,然后内核去访问磁盘中的文件,将文件内容返回给应用程序。大致的流程如下
在这里插入图片描述

直接缓冲区和非直接缓冲区

既然我们要读取一个磁盘的文件,要废这么大的周折。有没有什么简单的方法能够使我们的应用直接操作磁盘文件,不需要内核进行中转呢?有,那就是建立直接缓冲区了。

非直接缓冲区:非直接缓冲区就是我们上面所讲内核态作为中间人,每次都需要内核在中间作为中转。
在这里插入图片描述

直接缓冲区:直接缓冲区不需要内核空间作为中转copy数据,而是直接在物理内存申请一块空间,这块空间映射到内核地址空间和用户地址空间,应用程序与磁盘之间数据的存取通过这块直接申请的物理内存进行交互。
在这里插入图片描述

既然直接缓冲区那么快,我们为什么不都用直接缓冲区呢?其实直接缓冲区有以下的缺点。直接缓冲区的缺点:

  • 不安全
  • 消耗更多,因为它不是在JVM中直接开辟空间。这部分内存的回收只能依赖于垃圾回收机制,垃圾什么时候回收不受我们控制。
  • 数据写入物理内存缓冲区中,程序就丧失了对这些数据的管理,即什么时候这些数据被最终写入从磁盘只能由操作系统来决定,应用程序无法再干涉。

综上所述,所以我们使用transferTo方法就是直接开辟了一段直接缓冲区。所以性能相比而言提高了许多

使用内存映射文件

NIO中新出的另一个特性就是内存映射文件,内存映射文件为什么速度快呢?其实原因和上面所讲的一样,也是在内存中开辟了一段直接缓冲区。与数据直接作交互。源码如下

在这里插入图片描述

打印如下
在这里插入图片描述

可以看到速度和使用Channel的速度差不多的。

使用Pipe

Java NIO 管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。其中source通道用于读取数据,sink通道用于写入数据。可以看到源码中的介绍,大概意思就是写入线程会阻塞至有读线程从通道中读取数据。如果没有数据可读,读线程也会阻塞至写线程写入数据。直至通道关闭。

Whether or not a thread writing bytes to a pipe will block until another thread reads those bytes

我想要的效果是这样的。源码如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

源码地址
github.com/modouxiansh…

总结

生活处处都需要学习,有时候只是一个简单的优化,可以让你深入学习到各种不同的知识。所以在学习中要不求甚解,不仅要知道这个知识也要了解为什么要这么做。

本文转载自: 掘金

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

自定义注解很简单,只需掌握这几步! 什么是注解 Java内置

发表于 2021-03-07

文章已收录Github精选,欢迎Star:github.com/yehongzhi/l…

什么是注解

注解是JDK1.5引入的新特性,主要用于简化代码,提高编程的效率。其实在日常开发中,注解并不少见,比如Java内置的@Override、@SuppressWarnings,或者Spring提供的@Service、@Controller等等,随着这些注解使用的频率越来越高,作为开发人员当真有必要深入学习一番。

Java内置的注解

先说说Java内置的三个注解,分别是:

@Override:检查当前的方法定义是否覆盖父类中的方法,如果没有覆盖,编译器就会报错。

@SuppressWarnings:忽略编译器的警告信息。

@Deprecated:用于标识该类或方法已过时,建议开发人员不要使用该类或方法。

元注解

元注解其实就是描述注解的注解。主要有四个元注解,分别是:

@Target

用于描述注解的使用范围,也就是注解可以用在什么地方,取值有:

CONSTRUCTOR:用于描述构造器。

FIELD:用于描述字段。

LOCAL_VARIABLE:用于描述局部变量。

METHOD:用于描述方法。

PACKAGE:用于描述包。

PARAMETER:用于描述参数。

TYPE:用于描述类,包括class,interface,enum。

@Retention

表示需要在什么级别保存该注释信息,用于描述注解的生命周期,取值由枚举RetentionPoicy定义。

SOURCE:在源文件中有效(即源文件保留),仅出现在源代码中,而被编译器丢弃。

CLASS:在class文件中有效(即class保留),但会被JVM丢弃。

RUNTIME:JVM将在运行期也保留注释,因此可以通过反射机制读取注解的信息。

如果只是做一些检查性操作,使用SOURCE,比如@Override,@SuppressWarnings。

如果要在编译时进行一些预处理操作,就用 CLASS。

如果需要获取注解的属性值,去做一些运行时的逻辑,可以使用RUNTIME。

@Documented

将此注解包含在 javadoc 中 ,它代表着此注解会被javadoc工具提取成文档。它是一个标记注解,没有成员。

@Inherited

是一个标记注解,用来指定该注解可以被继承。使用 @Inherited 注解的 Class 类,表示这个注解可以被用于该 Class 类的子类。

自定义注解

下面实战一下,自定义一个注解@LogApi,用于方法上,当被调用时即打印日志,在控制台显示调用方传入的参数和调用返回的结果。

定义注解

首先定义注解@LogApi,在方法上使用,为了能在反射中读取注解信息,当然是设置为RUNTIME。

1
2
3
4
5
java复制代码@Target(value = ElementType.METHOD)
@Documented
@Retention(value = RetentionPolicy.RUNTIME)
public @interface LogApi {
}

这种没有属性的注解,属于标记注解。

多说几句,如果需要传递属性值,也可以设置属性值value,比如@RequestMapping注解。

1
2
3
4
5
6
7
8
java复制代码@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
@AliasFor("path")
String[] value() default {};
}

如果在使用时。只设置value值,可以忽略value,比如这样:

1
2
3
4
5
6
7
8
9
10
11
java复制代码//完整是@RequestMapping(value = {"/list"})
//忽略value不写
@RequestMapping("/list")
public Map<String, Object> list() throws Exception {
Map<String, Object> userMap = new HashMap<>();
userMap.put("1号佳丽", "李嘉欣");
userMap.put("2号佳丽", "袁咏仪");
userMap.put("3号佳丽", "张敏");
userMap.put("4号佳丽", "张曼玉");
return userMap;
}

标记注解

刚刚定义完注解之后,就可以在需要的地方标记注解,很简单。

1
2
3
4
5
java复制代码@LogApi
@RequestMapping("/list")
public Map<String, Object> list() throws Exception {
//业务代码...
}

解析注解

最关键的一步来了,解析注解,一般在项目中会使用Spring的AOP技术解析注解,当然如果只需要解析一次的话,也可以使用Spring容器的生命周期函数。

这里的场景是打印每次方法被调用的日志,所以使用AOP比较合适。

创建一个切面类LogApiAspect进行解析。

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
java复制代码@Aspect
@Component
public class LogApiAspect {
//切面点为标记了@LogApi注解的方法
@Pointcut("@annotation(io.github.yehongzhi.user.annotation.LogApi)")
public void logApi() {
}

//环绕通知
@Around("logApi()")
@SuppressWarnings("unchecked")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long starTime = System.currentTimeMillis();
//通过反射获取被调用方法的Class
Class type = joinPoint.getSignature().getDeclaringType();
//获取类名
String typeName = type.getSimpleName();
//获取日志记录对象Logger
Logger logger = LoggerFactory.getLogger(type);
//方法名
String methodName = joinPoint.getSignature().getName();
//获取参数列表
Object[] args = joinPoint.getArgs();
//参数Class的数组
Class[] clazz = new Class[args.length];
for (int i = 0; i < args.length; i++) {
clazz[i] = args[i].getClass();
}
//通过反射获取调用的方法method
Method method = type.getMethod(methodName, clazz);
//获取方法的参数
Parameter[] parameters = method.getParameters();
//拼接字符串,格式为{参数1:值1,参数2::值2}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
String name = parameter.getName();
sb.append(name).append(":").append(args[i]).append(",");
}
if (sb.length() > 0) {
sb.deleteCharAt(sb.lastIndexOf(","));
}
//执行结果
Object res;
try {
//执行目标方法,获取执行结果
res = joinPoint.proceed();
logger.info("调用{}.{}方法成功,参数为[{}],返回结果[{}]", typeName, methodName, sb.toString(), JSONObject.toJSONString(res));
} catch (Exception e) {
logger.error("调用{}.{}方法发生异常", typeName, methodName);
//如果发生异常,则抛出异常
throw e;
} finally {
logger.info("调用{}.{}方法,耗时{}ms", typeName, methodName, (System.currentTimeMillis() - starTime));
}
//返回执行结果
return res;
}
}

定义完切面类后,需要在启动类添加启动AOP的注解。

1
2
3
4
5
6
7
8
java复制代码@SpringBootApplication
//添加此注解,开启AOP
@EnableAspectJAutoProxy
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}

测试

我们再在Controller控制层增加一个有参数的接口。

1
2
3
4
5
6
7
8
9
java复制代码@LogApi
@RequestMapping("/get/{id}")
public String get(@PathVariable(name = "id") String id) throws Exception {
HashMap<String, Object> user = new HashMap<>();
user.put("id", id);
user.put("name", "关之琳");
user.put("经典角色", "十三姨");
return JSONObject.toJSONString(user);
}

启动项目,然后请求接口list(),我们可以看到控制台出现被调用方法的日志信息。

请求有参数的接口get(),可以看到参数名称和参数值都被打印在控制台。

这种记录接口请求参数和返回值的功能,在实际项目中基本上都会使用,因为这能利于系统的排错和性能调优等等。

我们也可以在这个例子中,学会使用注解和切面编程,可谓是一举两得!

总结

注解的使用能大大地减少开发的代码量,所以在实际项目的开发中会使用到非常多的注解。特别是做一些公共基础的功能,比如日志记录,事务管理,权限控制这些功能,使用注解就非常高效且优雅。

对于自定义注解,主要有三个步骤,定义注解,标记注解,解析注解,并不是很难。

这篇文章讲到这里了,感谢大家的阅读,希望看完这篇文章能有所收获!

文章持续更新,微信搜索『java技术爱好者』,关注后第一时间收到推送的技术文章,文章分类收录于github:github.com/yehongzhi,总能找到你感兴趣的

觉得有用就点个赞吧,你的点赞是我创作的最大动力~

我是一个努力让大家记住的程序员。我们下期再见!!!

能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流!

本文转载自: 掘金

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

strace报错 exec Exec format er

发表于 2021-03-07

问题:strace -o out.log ./print.sh 报错strace: exec: Exec format error。 print.sh文件内容
如下代码:

1
2
3
bash复制代码# set e
set -e
echo test trace print commander

原因:由于执行.sh文件时没有添加壳引用(shell reference),导致strace无法使用哪个shell来格式化打印信息。
解决。’参考’

a. .sh文件中增加壳引用如下代码。必须加载脚本开头。

1
2
3
4
bash复制代码#!/bin/bash 
# set e
set -e
echo test trace print commander

b. 命令中增加当前shell的调用。strace -o out.log $SHELL ./print.sh。

本文转载自: 掘金

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

基于SpringBoot+SpringSecurity+JW

发表于 2021-03-06

环境:Java8+SpringBoot 2.3.8+Spring Security 5.3.6 + java-jwt 3.12.0

主要是利用过滤器拦截(Filter)

实现了连接数据库,注册登录用户,使用Spring Security结合JWT实现安全认证。

前后端分离,使用json提交数据

Spring Security

Spring Security是一个功能强大、高度可定制的,并且专注于向Java应用提供**身份验证(authentication )和授权(authorization)**的框架。

authentication与authorization

authentication主要是确定你是谁,而authorization是可以赋予你访问资源,做某些事的权利。

JWT(JSON Web Token)

JWT主要是用来确定用户的身份信息。在用户第一次携带用户名和密码访问服务器时,服务器签发一个token给用户。接下来用户携带着这个token访问服务器,就不再需要用户密码再次登录。

具体可参考该博客
什么是 JWT – JSON WEB TOKEN

表数据结构

本次用例采用MongoDB,这里的数据库实现的并不是很重要,大家可以随意:)

tb_user

tb_role

tb_resource

tb_role_res

SpringBoot

新建一个SpringBoot项目

本项目使用了lombok插件

@Data主要用于生成getter和setter

@AllArgsConstructor生成全参构造函数

@NoArgsConstructor生成无参构造函数

@Document(value = "tb_user")关联MongoDB的表

@Id声明表的主键

  • 如果你不使用lombok插件,按照往常写法即可
  • 与表的关联等操作,按照对应数据库的方法操作

有关的Json操作使用了fastjson

maven

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.73</version>
</dependency>

pom.xml

完整的pom.xml文件

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
74
75
76
77
78
79
80
81
82
83
84
85
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.12.0</version>
</dependency>
<!-- json -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.73</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

entity

UserDo.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
java复制代码package com.example.demo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Document(value = "tb_user")
public class UserDo {

@Id
private String id; //Id
private String username; //用户名
private String password; //密码
private String role_name; //角色名

public UserDo(String username, String password, String role_name){
this.username = username;
this.password = password;
this.role_name = role_name;
}
}

RoleDo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码package com.example.demo.entity;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Data
@Document(value = "tb_role")
public class RoleDo {

@Id
private Integer id; //Id
private String name; //角色名
}

ResourceDo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码package com.example.demo.entity;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Data
@Document(value = "tb_resource")
public class ResourceDo {

@Id
private Integer id; //Id
private String name; //资源名
private String desc; //描述
}

RoleResourceDo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码package com.example.demo.entity;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Data
@Document(value = "tb_role_res")
public class RoleResourceDo {

@Id
private String id; //Id
private String role_name; //角色名
private String res_name; //资源名

}

dao

UserDao.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码package com.example.demo.dao;

import com.example.demo.entity.UserDo;
import org.springframework.data.mongodb.repository.MongoRepository;

public interface UserDao extends MongoRepository<UserDo,String> {

/**
* 根据用户名查询用户
* @param username 用户名
* @return {@code UserDo} 用户对象
*/
UserDo getUserDoByUsername(String username);
}

service

UserService.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
43
java复制代码package com.example.demo.service;

import com.example.demo.entity.UserDo;

import java.util.List;

public interface UserService {

/**
* 根据用户名获取用户
* @param username 用户名
* @return 用户存在时,返回{@code UserDo}用户对象,
* 否则返回{@code null}
*/
UserDo getUserByUsername(String username);

/**
* 根据用户名获取用户角色
* @param username 用户名
* @return {@code String[]} 角色字符串数组
*/
String[] getRolesByUser(String username);

/**
* 添加一个新用户
* @param username 用户名
* @param password 密码
* @return 创建成功返回{@code UserDo}
*/
UserDo addUser(String username, String password);

/**
* 获取全部用户列表
* @return {@code List<UserDo>} 用户列表
*/
List<UserDo> getUserList();

/**
* 根据Header中的token获取用户信息
* @return {@code UserDo} 用户信息
*/
UserDo getUserByToken();
}

impl

UserServiceImpl.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
43
44
45
46
47
48
49
50
51
52
53
54
java复制代码package com.example.demo.service.impl;

import com.example.demo.dao.UserDao;
import com.example.demo.entity.UserDo;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserServiceImpl implements UserService {

@Autowired
private UserDao userDao;

@Override
public UserDo getUserByUsername(String username) {
UserDo userDo = userDao.getUserDoByUsername(username);
if(userDo==null) return null;
return userDo;
}

@Override
public String[] getRolesByUser(String username) {
UserDo userDo = getUserByUsername(username);
String role_tmp = userDo.getRole_name();
String[] roles = role_tmp.split(",");
return roles;
}

@Override
public UserDo addUser(String username, String password) {
//使用Spring Security提供的BCryptPasswordEncoder加密用户密码存入数据库
//默认新加入的用户角色为user
return userDao.save(new UserDo(username, new BCryptPasswordEncoder().encode(password),"user"));
}

@Override
public List<UserDo> getUserList() {
return userDao.findAll();
}

@Override
public UserDo getUserByToken() {
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
User user = (User) authentication.getPrincipal();
return userDao.getUserDoByUsername(user.getUsername());
}
}

controller

UserController.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
43
44
45
46
47
48
49
50
51
52
java复制代码package com.example.demo.controller;

import com.alibaba.fastjson.JSONObject;
import com.example.demo.common.CodeMsg;
import com.example.demo.common.Result;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class UserController {

@Autowired
private UserService userService;

/**
* 获取全部用户信息列表
* @return
*/
@RequestMapping("/admin/getUserList")
@ResponseBody
public Result getUserList(){
return new Result(CodeMsg.SUCCESS,userService.getUserList());
}

/**
* 根据Header中的token获取用户信息
* @return
*/
@RequestMapping("/user/info")
@ResponseBody
public Result getUserInfo(){
return new Result(CodeMsg.SUCCESS,userService.getUserByToken());
}

/**
* 根据用户名和密码创建新用户
* @param jsonObject username,password
* @return
*/
@PostMapping("/register")
@ResponseBody
public Result register(@RequestBody JSONObject jsonObject){
String username = jsonObject.getString("username");
String password = jsonObject.getString("password");
return new Result(CodeMsg.REGISTER_SUCCESS,userService.addUser(username,password));
}
}

Spring Security

Spring Security是通过一系列的Filter(过滤器)来实现它的功能的

Filter的顺序与作用

以下是默认的Filter顺序

图片来源

在debug模式下可以看到过滤器执行的顺序

其中3,4是我自定义的过滤器,以下几种方法是用来添加过滤器的

1
2
3
4
5
6
java复制代码public HttpSecurity addFilterAfter(Filter filter, Class<? extends Filter> afterFilter);	   //在某个过滤器后添加一个过滤器
public HttpSecurity addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter); //在某个过滤器前添加一个过滤器
//添加一个过滤器,该过滤器必须继承filters中的过滤器,也就是该过滤器的自定义过滤器
public HttpSecurity addFilter(Filter filter)
//在某一个过滤器的同一位置添加一个过滤器,但该过滤器不会覆盖原有的过滤器;并且这两个过滤器的执行顺序是不确定的
public HttpSecurity addFilterAt(Filter filter, Class<? extends Filter> atFilter);

filters

  • ChannelProcessingFilter
  • SecurityContextPersistenceFilter
  • LogoutFilter
  • X509AuthenticationFilter
  • AbstractPreAuthenticatedProcessingFilter
  • CasAuthenticationFilter
  • UsernamePasswordAuthenticationFilter
  • OpenIDAuthenticationFilter
  • DefaultLoginPageGeneratingFilter
  • DefaultLogoutPageGeneratingFilter
  • ConcurrentSessionFilter
  • DigestAuthenticationFilter
  • BearerTokenAuthenticationFilter
  • BasicAuthenticationFilter
  • RequestCacheAwareFilter
  • SecurityContextHolderAwareRequestFilter
  • JaasApiIntegrationFilter
  • RememberMeAuthenticationFilter
  • AnonymousAuthenticationFilter
  • SessionManagementFilter
  • ExceptionTranslationFilter
  • FilterSecurityInterceptor
  • SwitchUserFilter

Spring Security Web 5.1.2 源码解析 – 安全相关Filter清单

SpringSecurity过滤器顺序

认证流程

图片来源

图片来源

以上两张图很清晰地展现了Spring Security的主要认证流程。

在接收到请求后,先经过一系列的过滤器。

当被UsernamePasswordAuthenticationFilter拦截到后,调用其attemptAuthentication(request,response)方法,获取到username和password后,封装成UsernamePasswordAuthenticationToken。

1
java复制代码UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username,password);

再让AuthenticationManager调用authenticate(authentication)方法进行验证。

1
java复制代码return this.getAuthenticationManager().authenticate(authenticationToken);

AuthenticationManager的默认实现是ProviderManager,其内部维护着一个AuthenticationProvider的列表。这个列表中存放的就是就是各种认证方式。当调用ProviderManager的authenticate(authentication)方法时,会遍历该列表。

当认证成功时,会返回一个经过身份验证的对象Authentication,并且不会执行后面的认证方法。若全部验证失败,则会抛出AuthenticationException异常。

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
java复制代码//部分ProviderManager的authenticate(Authentication authentication)方法源代码
for (AuthenticationProvider provider : getProviders()) { //遍历该列表
if (!provider.supports(toTest)) {
continue;
}

if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}

try {
result = provider.authenticate(authentication); //调用认证方法

if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
}

AuthenticationProvider的一个实现AbstractUserDetailsAuthenticationProvider会响应传过来的UsernamePasswordAuthenticationToken身份验证请求。

AbstractUserDetailsAuthenticationProvider调用authenticate(authentication)方法,根据传过来的Authentication获取用户名后,从缓存或者调用retrieveUser(username, authentication)方法。该方法由AbstractUserDetailsAuthenticationProvider的子类DaoAuthenticationProvider实现,主要是调用了UserDetailsService的loadUserByUsername(username)方法加载用户信息。我们一般会重写该方法,从数据库中取出用户信息。

在获取了正确的用户信息UserDetails和根据前面传数据过来后封装的UsernamePasswordAuthenticationToken后,调用DaoAuthenticationProvider实现的additionalAuthenticationChecks(userDetails,authentication)比较两者的密码是否一致完成验证。

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
java复制代码//retrieveUser方法源码
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
//此处可以看出调用了UserDetailsService().loadUserByUsername(username)
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}

如图是AuthenticationManager和AuthenticationProvider等几个类的关系

Json提交数据登录

因为Spring Security是默认通过form表单提交数据进行登录验证的。所以要通过URL访问后台并提交Json数据,需要修改Spring Security的配置。

在查阅资料的过程中,我发现在改写登录身份验证时,有的是从UsernamePasswordAuthenticationFilter继承,有的是从AbstractAuthenticationProcessingFilter继承。

UsernamePasswordAuthenticationFilter和AbstractAuthenticationProcessingFilter的区别

默认是使用UsernamePasswordAuthenticationFilter来拦截表单登录请求的,而UsernamePasswordAuthenticationFilter是从AbstractAuthenticationProcessingFilter继承而来的。

UsernamePasswordAuthenticationFilter

是用来处理表单登录的,默认登录URL为/login;需要提供两个参数:用户名和密码,也有默认的参数名,分别为username和password。要修改它的认证方法,主要是通过重写 attemptAuthentication(request,response)方法。

1
java复制代码public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response);

若是使用表单登录,可以在继承WebSecurityConfigurerAdapter的类里修改

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置登录请求相关内容
http.formLogin()
.usernameParameter("user")
.passwordParameter("pswd")
.loginPage("/toLogin") //登录页
.loginProcessingUrl("/login"); //登录表单提交地址
}
}

若是使用Json传递数据登录,可以参考里的JwtLoginFilter

AbstractAuthenticationProcessingFilter

是基于浏览器HTTP的身份验证请求处理器

继承该类时,主要需要设置三个地方

  1. 设置authenticationManager属性,是用来处理身份认证请求的token
  2. 需要设置RequestMatcher设置拦截登录用的URL
  3. 实现attemptAuthentication()方法
AuthenticationSuccessHandler

当验证成功时,会调用该Handler,默认实现是SavedRequestAwareAuthenticationSuccessHandler。它将用户重定向到ExceptionTranslationFilter中设置的DefaultSavedRequest,否则会重定向到Web应用程序的根目录。

也可以在验证成功后,重写successfulAuthentication()方法,调用顺序是先执行successfulAuthentication(),再执行Handler。要注意的是,如果重写successfulAuthentication()时,没有调用chain.doFilter(request, response),则不会再调用Handler

AuthenticationFailureHandler

默认实现是SimpleUrlAuthenticationFailureHandler。它向客户端发送401错误代码,也可以配置失败的URL

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
java复制代码package com.example.demo.filter;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.example.demo.util.ServletUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
* 自定义的身份验证过滤器
*/
public class CustomAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

public CustomAuthenticationFilter() {
//拦截 "/login" 的请求
super(new AntPathRequestMatcher("/login","POST"));
}

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
// 因request.getParameter()不能获取到application/json中的数据
// 需要把用户名密码的读取逻辑修改为到流中读取request.getInputStream()
String body = ServletUtil.getBody(request);
JSONObject jsonObject = JSON.parseObject(body);
String username = jsonObject.getString("username");
String password = jsonObject.getString("password");


if(username == null){
username = "";
}

if(password == null){
password = "";
}

username = username.trim();

UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username,password);

return this.getAuthenticationManager().authenticate(authenticationToken);
}
}

从数据库加载用户信息

上面提到过,DaoAuthenticationProvider的retrieveUser(username, authentication)方法中调用了UserDetailsService的loadUserByUsername(username)方法。所以我们继承UserDetailsService接口,实现该方法。

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.example.demo.service.impl;

import com.example.demo.entity.UserDo;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.Arrays;

/**
* 从数据库中加载用户信息
*/
@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService {

@Autowired
private UserService userService;

/**
*
* @param username 用户名
* @return {@code UserDetails}
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDo userDo = userService.getUserByUsername(username);
if(userDo==null){
throw new UsernameNotFoundException("用户不存在");
}
// 查询成功后,用户存在,需要匹配用户密码是否正确
// 匹配密码是由 Spring Security 内部逻辑自动完成
//用户登录成功后,查询用户的权限集合。
String[] roles = userService.getRolesByUser(username);

String[] authorities = new String[roles.length];
for(int i = 0; i < roles.length; i++){
authorities[i] = "ROLE_" + roles[i];
}

System.out.println("用户" + userDo.getUsername() + "的权限集合是:" + Arrays.toString(authorities));

org.springframework.security.core.userdetails.User result =
new org.springframework.security.core.userdetails.User(username,userDo.getPassword(), AuthorityUtils.createAuthorityList(authorities));

return result;
}
}

JWT

JWT官网里面有各种语言的JWT实现,这里选择的是auth0的

maven

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.12.0</version>
</dependency>

JWT工具类

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
java复制代码package com.example.demo.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Calendar;
import java.util.Date;
import java.util.function.Function;

/**
* jwt 工具类
*/
public class JwtTokenUtil {

public static final int JWT_TOKEN_VALIDITY = 30; //Token有效期,单位:分钟

public static final String SECRET = "Secret"; //私钥

/**
* 根据token获取用户名
* @param token
* @return {@code String} 用户名
*/
public static String getAudienceByToken(String token){
String audience = null;
try {
audience = JWT.decode(token).getAudience().get(0);
}catch (JWTDecodeException e){
e.printStackTrace();
throw new JWTDecodeException("jwt token解码失败");
}
return audience;
}

/**
* 根据token和实体名获取自定义实体
* @param token
* @param name 实体名
* @return {@code Claim}
*/
public static Claim getClaimByName(String token, String name){
return JWT.decode(token).getClaim(name);
}

/**
* 根据用户名和私钥生成token
* @param username 用户名
* @return {@code String} token
*/
public static String generateToken(String username){
String token = null;

Calendar nowTime = Calendar.getInstance();
nowTime.add(Calendar.MINUTE,JWT_TOKEN_VALIDITY);
Date expiresDate = nowTime.getTime();
try {
token = JWT.create()
.withAudience(username) //签发对象
.withIssuedAt(new Date()) //发行时间
.withExpiresAt(expiresDate)//有效时间
.sign(Algorithm.HMAC256(username+SECRET)); //加密算法

}catch (JWTCreationException exception){
exception.printStackTrace();
}
return token;
}

/**
* 根据token和用户名验证token是否正确
* @param token
* @param username 用户名
* @return {@code true} token正确
* {@code false} token错误
*/
public static Boolean validateToken(String token, String username){
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(username+SECRET))
.withAudience(username)
.build();
DecodedJWT jwt = verifier.verify(token);
return true;
}catch (TokenExpiredException e){
e.printStackTrace();
return false;
}catch (JWTVerificationException e){
e.printStackTrace();
return false;
}
}
}

添加JWT Token

在通过身份验证后,我们为利用用户的用户名和私钥生成一个Jwt token返回给前台。

新建一个JsonLoginSuccessHandler类来处理该逻辑

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
java复制代码package com.example.demo.handler;

import com.example.demo.util.JwtTokenUtil;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
* 身份验证后,在header中添加jwt token
*/
public class JsonLoginSuccessHandler implements AuthenticationSuccessHandler {


@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

//添加token
String token = JwtTokenUtil.generateToken(((UserDetails)authentication.getPrincipal()).getUsername());
response.addHeader("Authorization",token);
}
}

配置Filter

配置一个登录失败的处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码package com.example.demo.handler;

import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
* 登录失败处理器
* 回复401
*/
public class HttpStatusLoginFailureHandler implements AuthenticationFailureHandler {

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
}
}

配置Filter

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复制代码package com.example.demo.config;

import com.example.demo.filter.CustomAuthenticationFilter;
import com.example.demo.handler.HttpStatusLoginFailureHandler;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;

/**
* 配置CustomAuthenticationFilter
*/
public class JsonLoginConfig<T extends JsonLoginConfig<T, B>, B extends HttpSecurityBuilder<B>> extends AbstractHttpConfigurer<T, B> {

private CustomAuthenticationFilter authFilter;

public JsonLoginConfig(){
this.authFilter = new CustomAuthenticationFilter();
}

@Override
public void configure(B builder) throws Exception {
//设置Filter使用的AuthenticationManager,这里取公共的
authFilter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));
//设置失败的Handler
authFilter.setAuthenticationFailureHandler(new HttpStatusLoginFailureHandler());
//不将认证后的context放入session
authFilter.setSessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy());

CustomAuthenticationFilter authenticationFilter = postProcess(authFilter);
//指定filter的位置
builder.addFilterAfter(authenticationFilter, LogoutFilter.class);
}

//设置成功的Handler,这个handler定义成Bean,所以从外面set进来
public JsonLoginConfig<T,B> loginSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler){
authFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
return this;
}
}

验证Jwt Token

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
74
75
76
77
78
79
80
81
82
83
java复制代码package com.example.demo.filter;

import com.auth0.jwt.exceptions.TokenExpiredException;
import com.example.demo.service.impl.JwtUserDetailsServiceImpl;
import com.example.demo.util.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
* 每个需要验证的请求都验证token是否正确
*/
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

@Autowired
private JwtUserDetailsServiceImpl jwtUserDetailsServiceImpl;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
System.out.println("JwtAuthorizationFilter执行...");
System.out.println("验证Token...");
UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
if(authentication == null){
System.out.println("authentication null");
filterChain.doFilter(request,response);
return;
}

SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request,response);
}

/**
* 根据request中的token,验证后获取UsernamePasswordAuthenticationToken
* @param request
* @return
*/
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request){

String username = null;
String jwtToken = null;

jwtToken = request.getHeader("Authorization");

if(jwtToken != null){
try {
username = JwtTokenUtil.getAudienceByToken(jwtToken);
}catch (IllegalArgumentException e){
e.printStackTrace();
System.out.println("不能获取token或token不正确");
}catch (TokenExpiredException e){
e.printStackTrace();
System.out.println("token过期");
}
}

//获取token后验证
if(username != null && SecurityContextHolder.getContext().getAuthentication() == null){
System.out.println("username:"+username);

UserDetails userDetails = jwtUserDetailsServiceImpl.loadUserByUsername(username);
if(JwtTokenUtil.validateToken(jwtToken,username)){ //验证token
//创建UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken= new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
return usernamePasswordAuthenticationToken;
}
}

return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码package com.example.demo.config;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
* 拒绝每个未经身份验证(token)的请求并发送错误代码401
*/
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,"Unauthorized");
System.out.println("JWT Unauthorized...");
}
}

Security配置

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

import com.example.demo.filter.JwtAuthenticationFilter;
import com.example.demo.handler.JsonLoginSuccessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;

@Override
protected void configure(HttpSecurity http) throws Exception {
//super.configure(http);

//禁用form登录
http.formLogin().disable();


// 配置权限
http.authorizeRequests()
.antMatchers("/login","/register").permitAll()
// 基于角色的权限管理
.antMatchers("/admin/**","/user/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.anyRequest().authenticated(); //任意的请求,都必须认证后才能访问

// 添加过滤器
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

http.apply(new JsonLoginConfig<>()).loginSuccessHandler(jsonLoginSuccessHandler());

http.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint);

//使用无状态session,session不会储存用户状态
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);


//关闭CSRF安全协议
//关闭是为了保证完整流程的可用
http.csrf().disable();
}

// 注入密码编码器对象
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}

@Bean
protected JsonLoginSuccessHandler jsonLoginSuccessHandler(){
return new JsonLoginSuccessHandler();
}
}

通用类

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
java复制代码package com.example.demo.common;

import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor
public enum CodeMsg {

REGISTER_SUCCESS(2000,"注册成功"),
REGISTER_FAILURE(2001,"注册失败"),

LOGIN_SUCCESS(2002,"登录成功"),
LOGIN_FAILURE(2003,"登录失败"),

SUCCESS(2004,"获取数据成功"),
FAILURE(2005,"获取数据失败")


;

private int code;
private String message;

public int getCode() {
return code;
}

public void setCode(int code) {
this.code = code;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码package com.example.demo.common;

import lombok.Data;

@Data
public class Result {

private int code;
private String message;
private Object entity;

public Result(){}
public Result(CodeMsg codeMsg, Object entity){
this.code=codeMsg.getCode();
this.message=codeMsg.getMessage();
this.entity=entity;
}

}
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
java复制代码package com.example.demo.util;

import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;

public class ServletUtil {

/**
* 获取请求中的body
* @param request
* @return
*/
public static String getBody(HttpServletRequest request){
StringBuilder stringBuilder = new StringBuilder();
InputStream inputStream = null;
BufferedReader reader = null;
try{
inputStream = request.getInputStream();
reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
String line = "";
while ((line = reader.readLine())!=null){
stringBuilder.append(line);
}
}catch (IOException e){
e.printStackTrace();
}finally {
if(inputStream != null){
try {
inputStream.close();
}catch (IOException e){
e.printStackTrace();
}
}
if(reader != null){
try{
reader.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
return stringBuilder.toString();
}
}

测试

使用Postman进行测试

使用管理员的账号密码进行登录,可以看到返回的header中带有Authorization

下一次请求带上该token,成功获取数据

相同的操作,更换成普通用户登录。

获取token后访问只有管理员能访问的接口,可以看到返回了403没有权限

如果不带token访问,会返回401没有授权

小结

本文主要描述了使用如何使用Spring Security和JWT结合Springboot进行登录验证。实现这个功能,还有多种配置方式。可以使用Form表单登录,前后端结合在一起。还有更多个性化的配置,如不使用UsernamePasswordAuthenticationToken,新建一个Token类;还有使用多种验证方式,邮箱登录等。

参考

Spring Security 案例实现和执行流程剖析

Spring Security做JWT认证和授权

自定义SpringSecurity认证方式

SpringSecurity 核心组件介绍 + 认证流程 +内置拦截器顺序

Spring-Security权限框架

尚硅谷SpringSecurity框架教程

本文转载自: 掘金

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

线程池如何传递ThreadLocal

发表于 2021-03-06

file

前言

在做分布式链路追踪系统的时候,需要解决异步调用透传上下文的需求,特别是传递traceId,本文就线程池透传几种方式进行分析。

其他典型场景例子:

  1. 分布式跟踪系统 或 全链路压测(即链路打标)
  2. 日志收集记录系统上下文
  3. Session级Cache
  4. 应用容器或上层框架跨应用代码给下层SDK传递信息

1、JDK对跨线程传递ThreadLocal的支持

首先看一个最简单场景,也是一个错误的例子。

1
2
3
4
5
6
7
java复制代码    void testThreadLocal(){
ThreadLocal<Object> threadLocal = new ThreadLocal<>();
threadLocal.set("not ok");
new Thread(()->{
System.out.println(threadLocal.get());
}).start();
}

java中的threadlocal,是绑定在线程上的。你在一个线程中set的值,在另外一个线程是拿不到的。

上面的输出是:

null

1.1 InheritableThreadLocal 例子

JDK考虑了这种场景,实现了InheritableThreadLocal ,不要高兴太早,这个只是支持父子线程,线程池会有问题。

我们看下InheritableThreadLocal的例子:

1
2
3
4
5
6
7
8
9
10
11
java复制代码        InheritableThreadLocal<String> itl = new InheritableThreadLocal<>();
itl.set("father");
new Thread(()->{
System.out.println("subThread:" + itl.get());
itl.set("son");
System.out.println(itl.get());
}).start();

Thread.sleep(500);//等待子线程执行完

System.out.println("thread:" + itl.get());

上面的输出是:

subThread:father //子线程可以拿到父线程的变量

son

thread:father //子线程修改不影响父线程的变量

1.2 InheritableThreadLocal的实现原理

有同学可能想知道InheritableThreadLocal的实现原理,其实特别简单。就是Thread类里面分开记录了ThreadLocal、InheritableThreadLocal的ThreadLocalMap,初始化的时候,会拿到parent.InheritableThreadLocal。直接上代码可以看的很清楚。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码class Thread {
...
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

...

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}

JDK的InheritableThreadLocal类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把 任务提交给线程池时的ThreadLocal值传递到 任务执行时。

2、日志MDC/Opentracing的实现

如果你的应用实现了Opentracing的规范,比如通过skywalking的agent对线程池做了拦截,那么自定义Scope实现类,可以跨线程传递MDC,然后你的义务可以通过设置MDC的值,传递给子线程。

代码如下:

1
2
3
4
5
6
7
8
9
10
java复制代码        this.scopeManager = scopeManager;
this.wrapped = wrapped;
this.finishOnClose = finishOnClose;
this.toRestore = (OwlThreadLocalScope)scopeManager.tlsScope.get();
scopeManager.tlsScope.set(this);
if (wrapped instanceof JaegerSpan) {
this.insertMDC(((JaegerSpan)wrapped).context());
} else if (wrapped instanceof JaegerSpanWrapper) {
this.insertMDC(((JaegerSpanWrapper)wrapped).getDelegated().context());
}

3、阿里transmittable-thread-local

github地址:github.com/alibaba/tra…

TransmittableThreadLocal(TTL)是框架/中间件缺少的Java™std lib(简单和0依赖),提供了增强的InheritableThreadLocal,即使使用线程池组件也可以在线程之间传输值。

3.1 transmittable-thread-local 官方readme参考:

使用类TransmittableThreadLocal来保存值,并跨线程池传递。

TransmittableThreadLocal继承InheritableThreadLocal,使用方式也类似。相比InheritableThreadLocal,添加了

  1. copy方法
    用于定制 任务提交给线程池时 的ThreadLocal值传递到 任务执行时 的拷贝行为,缺省传递的是引用。
    注意:如果跨线程传递了对象引用因为不再有线程封闭,与InheritableThreadLocal.childValue一样,使用者/业务逻辑要注意传递对象的线程
  2. protected的beforeExecute/afterExecute方法
    执行任务(Runnable/Callable)的前/后的生命周期回调,缺省是空操作。

3.2 transmittable-thread-local 代码例子

方式一:TtlRunnable封装:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码ExecutorService executorService = Executors.newCachedThreadPool();
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();

// =====================================================
// 在父线程中设置
context.set("value-set-in-parent");

// 额外的处理,生成修饰了的对象ttlRunnable
Runnable ttlRunnable = TtlRunnable.get(() -> {
System.out.println(context.get());
});
executorService.submit(ttlRunnable);

方式二:ExecutorService封装:

1
2
3
java复制代码ExecutorService executorService = ...
// 额外的处理,生成修饰了的对象executorService
executorService = TtlExecutors.getTtlExecutorService(executorService);

方式三:使用java agent,无代码入侵

这种方式,实现线程池的传递是透明的,业务代码中没有修饰Runnable或是线程池的代码。即可以做到应用代码 无侵入。

1
2
3
4
5
6
7
8
9
java复制代码ExecutorService executorService = Executors.newCachedThreadPool();
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================
// 在父线程中设置
context.set("value-set-in-parent");

executorService.submit(() -> {
System.out.println(context.get());
});

4、grpc的实现

grpc是一种分布式调用协议和实现,也封装了一套跨线程传递上下文的实现。

io.grpc.Context 表示上下文,用来在一次grpc请求链路中传递用户登录信息、tracing信息等。

Context常用用法如下。首先获取当前context,这个一般是作为参数传过来的,或通过current()获取当前的已有context。

然后通过attach方法,绑定到当前线程上,并且返回当前线程

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码    public Runnable wrap(final Runnable r) {
return new Runnable() {
@Override
public void run() {
Context previous = attach();
try {
r.run();
} finally {
detach(previous);
}
}
};
}

Context的主要方法如下

  • attach() attach Context自己,从而进入到一个新的scope中,新的scope以此Context实例作为current,并且返回之前的current context
  • detach(Context toDetach) attach()方法的反向方法,退出当前Context并且detach到toDetachContext,每个attach方法要对应一个detach,所以一般通过try finally代码块或wrap模板方法来使用。
  • static storage() 获取storage,Storage是用来attach和detach当前context用的。

线程池传递实现:

1
2
3
4
5
6
java复制代码ExecutorService executorService = Executors.newCachedThreadPool();
Context.withValue("key","value");

execute(Context.current().wrap(() -> {
System.out.println(Context.current().getValue("key"));
}));

5、总结

以上总结的四种实现跨线程传递的方法,最简单的就是自己定义一个Runnable,添加属性传递即可。如果考虑通用型,需要中间件封装一个Executor对象,类似transmittable-thread-local的实现,或者直接使用transmittable-thread-local。

实践的项目中,考虑周全,要支持span、MDC、rpc上下文、业务自定义上下文,可以参考以上方法封装。

参考资料

[grpc源码分析1-context] www.codercto.com/a/66559.htm…

[threadlocal变量透传,这些问题你都遇到过吗?]cloud.tencent.com/developer/a…

扫描二维码,关注公众号“猿必过”

回复 “面试题” 自行领取吧。

微信群交流讨论,请添加微信号:zyhui98,备注:面试题加群

本文由猿必过 YBG 发布
禁止未经授权转载,违者依法追究相关法律责任
如需授权可联系:zhuyunhui@yuanbiguo.com

本文转载自: 掘金

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

震惊!JDK7和JDK8中关于ForkJoinPool的内存

发表于 2021-03-06

Bug地址:bugs.java.com/bugdatabase…

背景

由于当时在解决了一个问题:Tomcat容器应用中使用CompletableFuture时,关于ClassLoader引起的问题,之后,后来有时间对此此问题中的一些细节进行一个补充!

透过ForkJoinPool源码了解这个BUG

首先走一遍过程,来看下在Tomcat中ForkJoinPool的默认线程池的线程工程是怎么变为SafeForkJoinWorkerThreadFactory的。

###1)首先查看ForkJoinPool设置ThreadFactory的地方源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ini复制代码private static ForkJoinPool makeCommonPool() {
int parallelism = -1;
ForkJoinWorkerThreadFactory factory = null;
UncaughtExceptionHandler handler = null;
try { // ignore exceptions in accessing/parsing properties
、、、、、、省略不重要代码
String fp = System.getProperty
("java.util.concurrent.ForkJoinPool.common.threadFactory");
if (fp != null)
factory = ((ForkJoinWorkerThreadFactory)ClassLoader.
getSystemClassLoader().loadClass(fp).newInstance());
、、、、、、省略不重要代码
} catch (Exception ignore) {
}
if (factory == null) {
if (System.getSecurityManager() == null)
factory = defaultForkJoinWorkerThreadFactory;
else // use security-managed default
factory = new InnocuousForkJoinWorkerThreadFactory();
}
、、、、、、省略不重要代码

可以看到,如果可以从java.util.concurrent.ForkJoinPool.common.threadFactory中获取到值,那么就使用这个值作为ThreadFactory,相当于是一个扩展。

2)那么这个值是在哪里设置进去的呢?

设置的地方就是org.apache.catalina.core.JreMemoryLeakPreventionListener。这个类实现了LifecycleListener。其中有一行代码

1
2
3
4
5
6
7
less复制代码                if (forkJoinCommonPoolProtection && !JreCompat.isJre9Available()) {
// Don't override any explicitly set property
if (System.getProperty(FORK_JOIN_POOL_THREAD_FACTORY_PROPERTY) == null) {
System.setProperty(FORK_JOIN_POOL_THREAD_FACTORY_PROPERTY,
SafeForkJoinWorkerThreadFactory.class.getName());
}
}

3)为什么Tomcat要做这种操作?

到这里就引出了文章标题中所说的JDK7和JDK8中关于ForkJoinPool的BUG!为啥是JDK7和JDK8?因为ForkJoinPool是JDK7开始存在,那么之前的版本自然没有,而JDK9之后针对此BUG进行了修复。

先说下这个BUG:首先当我们使用诸如CompletableFuture时,使用它的一些runAsync之类方式时,如果我们不默认指定线程池,则会使用ForkJoinPool.commonPool()。

1
2
3
4
5
6
java复制代码private static final Executor asyncPool = useCommonPool ?
ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();

public static CompletableFuture<Void> runAsync(Runnable runnable) {
return asyncRunStage(asyncPool, runnable);
}

也就是说在整个JVM中,当我们的代码使用了像CompletableFuture或者一些parallelStream等时,会默认使用ForkJoinPool,且共用一个ForkJoinPool,这看起来在我们普通的Java SE程序中好像不会有什么问题,也确实不会有问题,但是在JAVA EE环境下就不同了,比如我们常见的JAVA WEB程序会放在Tomcat中运行,而Tomcat为了达到不同应用的隔离,其实是会为WebApp下每一个应用创建一个专属ClassLoader加载执行。那么基于上述机制,不同的应用中最终会使用同一个ForkJoinPool去执行处理代码。

可能此时还有点懵逼?接着往下看bug所在,在JDK7和JDK8中,看下其ThreadFactory的实现。

JDK8中:

1
2
3
4
5
6
java复制代码    static final class DefaultForkJoinWorkerThreadFactory
implements ForkJoinWorkerThreadFactory {
public final ForkJoinWorkerThread newThread(ForkJoinPool pool) {
return new ForkJoinWorkerThread(pool);
}
}

可以看到是搞了一个静态内部类,这里工厂产生线程时,直接new ForkJoinWorkerThread(pool);再来看下ForkJoinWorkerThread的构造方法:

1
2
3
4
5
6
kotlin复制代码    protected ForkJoinWorkerThread(ForkJoinPool pool) {
// Use a placeholder until a useful name can be set in registerWorker
super("aForkJoinWorkerThread");
this.pool = pool;
this.workQueue = pool.registerWorker(this);
}

ForkJoinWorkerThread是继承了Thread,这构造方法中直接调用 super(“aForkJoinWorkerThread”);而这个构造方法中,新线程的contextClassLoader会继承父线程的contextClassLoader,这里就是BUG所在,为什么这么说,在Tomcat应用中,父线程的contextClassLoader自然就是WebAppClassLoader,WebApp下每个应用都有一个,那么如果在产生的新线程中使用contextClassLoader去加载一些类使用,后来这个应用可能要卸载,但是其拽你书WebAppClassLoader依然被ForkJoinPool中的线程所持有,所以GC无法回收,进而也无法回收这个应用中加载的一些资源,从而造成内存泄漏。

4)BUG是如何修复的?

直接看下JDK9中的实现:

ForkJoinPool.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码    private static final class DefaultForkJoinWorkerThreadFactory
implements ForkJoinWorkerThreadFactory {
private static final AccessControlContext ACC = contextWithPermissions(
new RuntimePermission("getClassLoader"),
new RuntimePermission("setContextClassLoader"));

public final ForkJoinWorkerThread newThread(ForkJoinPool pool) {
return AccessController.doPrivileged(
new PrivilegedAction<>() {
public ForkJoinWorkerThread run() {
return new ForkJoinWorkerThread(
pool, ClassLoader.getSystemClassLoader()); }},
ACC);
}
}

可以看到与1.8之前不同,这里在新建ForkJoinWorkerThread时,直接手动传入了ClassLoader.getSystemClassLoader()作为contextClassLoader

再来从Tomcat8中的版本更新中看下Tomcat针对此问题的应对措施,具体见tomcat.apache.org/tomcat-8.5-…

  • Tomcat 8.5.11 在中提供Tomcat容器生命周期监听类的实现JreMemoryLeakPreventionListener修复此问题
  • Tomcat 8.5.30 中,由于JDK9中修复了此BUG,所以在JreMemoryLeakPreventionListener中增加了开关判断,如果当前JVM支持JDK9,则不使用SafeForkJoinWorkerThreadFactory。

本文转载自: 掘金

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

1…710711712…956

开发者博客

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