分布式事务讲解之CAP,2PC,3PC,TCC,分布式锁
学习此篇分布式事务前请先学习Spring事务讲解
点击了解Spring事务讲解
1 CAP
1.1 CAP原则
CAP
原则又称CAP
定理, 指的是在一个分布式系统
中, Consistency
(一致性) 、Availability
(可用性) 、 Partition tolerance
(分区容错性) , 三者不可兼得。
原则分类 | 详解 |
---|---|
C 数据一致性( Consistency ) |
也叫做数据原子性系统在执行某项操作后仍然处于一致的状态。 在分布式系统中, 更新操作执行成功后所有的用户都应该读到最新的值,这样的系统被认为是具有强一致性 的,也就是每个时刻都必须一样,不一样整个系统就不能对外提供服务。 等同于所有节点访问同一份最新的数据副本 |
A 服务可用性( Availablity ) |
每一个操作总是能够在一定的时间内返回结果, 这里需要注意的是一定时间内 和返回结果 。 一定时间内指的是,在可以容忍的范围内返回结果, 结果可以是成功或者是失败 |
P 分区容错性( Partition-torlerance ) |
在网络分区的情况下, 被分隔的节点仍能正常对外提供服务(分布式集群, 数据被分布存储在不同的服务器上, 无论什么情况, 服务器都能正常被访问) |
分区容错性
重点讲解:一个分布式系统里面,节点组成的网络本来应该是连通的。然而可能因为一些故障,使得有些节点之间不连通了,整个网络就分成了几块区域。数据就散布在了这些不连通的区域中。这就叫分区。当一个数据项只在一个节点中保存,那么分区出现后,和这个节点不连通的部分就访问不到这个数据了。这是分区就是无法容忍的。提高分区容忍性的办法就是一个数据项复制到多个节点上,那么出现分区之后,这一数据项就可能分布到各个区里。容忍性就提高了。然而,要把数据复制到多个节点,就会带来一致性的问题,就是多个节点上面的数据可能是不一致的。要保证一致,每次写操作就都要等待全部节点写成功,而这等待又会带来可用性的问题。
总的来说就是,数据存在的节点越多,分区容忍性越高,但要复制更新的数据就越多,一致性就越难保证。为了保证一致性,更新所有节点数据所需要的时间就越长,可用性就会降低
1.1.1 数据一致性
数据一致性的种类:
强一致性
(线性一致性):即复制是同步的
任何一次读都能读到某个数据的最近一次写的数据
系统中的所有进程,看到的操作顺序,都和全局时钟下的顺序一致
简言之,在任意时刻,所有节点中的数据是一样的弱一致性
:即复制是异步的
数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性
最终一致性就属于弱一致性
最终一致性
不保证在任意时刻任意节点上的同一份数据都是相同的,但是随着时间的迁移,不同节点上的同一份数据总是在向趋同的方向变化。
最终
两个字用得很微妙,因为从写入主库到反映至从库之间的延迟,可能仅仅是几分之一秒,也可能是几个小时
简单说,就是在一段时间后,节点间的数据会最终达到一致状态
1.1.2 图示讲解
让我们来考虑一个非常简单的分布式系统,它由两台服务器G1和G2组成;这两台服务器都存储了同一个变量v,v的初始值为v0;G1和G2互相之间能够通信,并且也能与外部的客户端通信;我们的分布式系统的架构图如下图所示:
一个简单的分布式系统
客户端可以向任何服务器发出读写请求。服务器当接收到请求之后,将根据请求执行一些计算,然后把请求结果返回给客户端。譬如,下图是一个写请求的例子:
客户端发起写请求
接着,下图是一个读请求的例子
客户端发起读请求
现在我们的分布式系统建立起来了,下面我们就来回顾一下分布式系统的可用性、一致性以及分区容错性的含义。
1.1.2.1 一致性
在一个一致性的系统中,客户端向任何服务器发起一个写请求,将一个值写入服务器并得到响应,那么之后向任何服务器发起读请求,都必须读取到这个值(或者更加新的值)。
下图是一个不一致的分布式系统的例子:
客户端向G1发起写请求,将v的值更新为v1且得到G1的确认响应;当向G2发起读v的请求时,读取到的却是旧的值v0,与期待的v1不一致。
下图一致的分布式系统的例子:
在这个系统中,G1在将确认响应返回给客户端之前,会先把v的新值复制给G2,这样,当客户端从G2读取v的值时就能读取到最新的值v1
1.1.2.2 可用性
在一个可用的分布式系统中,客户端向其中一个服务器发起一个请求且该服务器未崩溃,那么这个服务器最终必须响应客户端的请求。
1.1.2.3 分区容错性
服务器G1和G2之间互相发送的任意消息都可能丢失。如果所有的消息都丢失了,那么我们的系统就变成了下图这样:
为了满足分区容错性,我们的系统在任意的网络分区情况下都必须正常的工作
1.2 CAP如何舍弃
定律
: 任何分布式系统只可同时满足二点,没法三者兼顾
,对于 分布式数据系统,分区容忍性是基本要求,否则就失去了价值
三者择其二 | 分析 |
---|---|
CA, 放弃 P |
如果想避免分区容错性问题的发生, 一种做法是将所有的数据(与事务相关的)都放在一台机器上。 虽然无法 100%保证系统不会出错, 但不会碰到由分区带来的负面效果。 当然这个选择会严重的影响系统的扩展性 |
CP, 放弃 A |
相对于放弃"分区容错性"来说, 其反面就是放弃可用性。一旦遇到分区容错故障, 那么受到影响的服务需要等待一定时间, 因此在等待时间内系统无法对外提供服务 |
AP, 放弃 C |
这里所说的放弃一致性, 并不是完全放弃数据一致性,而是放弃数据的强一致性, 而保留数据的最终一致性。 以网络购物为例, 对只剩下一件库存的商品, 如果同时接受了两个订单, 那么较晚的订单将被告知商品告罄 |
1.3 eureka与zookeeper区别
对比项 | Zookeeper | Eureka | |
---|---|---|---|
CAP | CP | AP | |
Dubbo 集成 | 已支持 | - | |
Spring Cloud 集成 | 已支持 | 已支持 | |
kv 服务 | 支持 | - | ZK 支持数据存储,eureka不支持 |
使用接口(多语言能力) | 提供客户端 | http 多语言 | ZK的跨语言支持比较弱 |
watch 支持 | 支持 | 支持 | 什么是Watch 支持?就是客户单监听服务端的变化情况。zk 通过订阅监听来实现eureka 通过轮询的方式来实现 |
集群监控 | - | metrics | metrics,运维者可以收集并报警这些度量信息达到监控目的 |
1.4 CAP对应的模型和应用
1.4.1 CA without P
理论上放弃P(分区容错性),则C(强一致性)和A(可用性)是可以保证的。实际上分区是不可避免的,严格上CA指的是允许分区后各子系统依然保持CA。
CA模型的常见应用:
- 集群数据库
- xFS文件系统
1.4.2 CP without A
放弃A(可用),相当于每个请求都需要在Server之间强一致,而P(分区)会导致同步时间无限延长,如此CP也是可以保证的。很多传统的数据库分布式事务都属于这种模式。
CP模型的常见应用:
- 分布式数据库
- 分布式锁
1.4.3 AP wihtout C
要高可用并允许分区,则需放弃一致性。一旦分区发生,节点之间可能会失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致性。现在众多的NoSQL都属于此类。
AP模型常见应用:
- Web缓存
- DNS
1.4.4 常见注册中心
举个大家更熟悉的例子,像我们熟悉的注册中心ZooKeeper
、Eureka
、Nacos
中:
ZooKeeper
保证的是 CPEureka
保证的则是 APNacos
不仅支持 CP 也支持 AP
1.5 BASE理论
BASE(Basically Available、Soft state、Eventual consistency)
是基于CAP理论逐步演化而来的,核心思想是即便不能达到强一致性(Strong consistency
),也可以根据应用特点采用适当的方式来达到最终一致性(Eventual consistency
)的效果。
权衡了可用性
和一致性
而提出的理论,相当于满足PA
的情况(并没有完全舍弃C
,只是舍弃强一致性
,保留了最终一致性
)
BASE
的主要含义:
Basically Available
(基本可用)
什么是基本可用呢?假设系统出现了不可预知的故障,但还是能用,只是相比较正常的系统而言,可能会有响应时间上的损失,或者功能上的降级。
响应时间上
:可能因为网络故障导致响应时间延长一点点
功能上
:由于某个服务突然被大量访问,那新来的访问被降级到其他服务,如返回网络繁忙等等Soft State
(软状态)
什么是硬状态呢?要求多个节点的数据副本都是一致的,这是一种硬状态
。
软状态
也称为弱状态
,相比较硬状态而言,允许系统中的数据存在中间状态
,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。
即:允许系统中的某些数据处于中间状态
,且这些中间状态不影响整体的可用性,即允许各个节点之间的数据同步存在延迟Eventually Consistent
(最终一致性)
上面说了软状态,但是不应该一直都是软状态。在一定时间后,应该到达一个最终的状态,保证所有副本保持数据一致性,从而达到数据的最终一致性
。这个时间取决于网络延时、系统负载、数据复制方案设计等等因素。
2 分布式事务
现在实现分布式事务的设计方案应该有3种,分别就是二阶段提交、三阶段提交和TCC
他们都有2种重要的角色 事务协调者
和参与者
,也就是各个服务
2.1 二阶段提交(2PC)
两阶段提交的思路可以概括为:
参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情况决定各参与者是否要提交操作还是回滚操作。
2.1.1 准备阶段
准备阶段:协调者
(事务管理器)要求每个涉及到事务的数据库参与者
预提交(precommit)此操作,并反映是否可以提交
根据上面的UML
图来看
1.3
反馈准备提交或回滚 存在多种情况:
- 所有参与者都反馈可以提交
- 有参与者反馈回滚(不管多少)
2.1.2. 提交阶段
提交阶段:协调者
(事务管理器)要求每个数据库参与者
提交数据或者回滚数据
根据上面的UML
图来看
2.1
根据1.3
的反馈情况:- 所有参与者都反馈可以提交 –> 通知全部提交
- 有参与者反馈回滚 –> 通知全部回滚
- 等待反馈超时 –> 通知全部回滚
2.2
根据2.1
:- 通知全部提交 –> 提交
- 通知全部回滚 –> 回滚
- 一直没收到请求 –> 阻塞住
2.1.3 两阶段优缺点
优点:尽量保证了数据的强一致,实现成本较低,在各大主流数据库都有自己实现,对于MySQL是从5.5开始支持。
缺点:
单点问题
事务管理器在整个流程中扮演的角色很关键,如果其宕机,比如在第一阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,资源管理器就会一直阻塞,导致数据库无法使用。同步阻塞
在准备就绪之后,资源管理器中的资源一直处于阻塞,直到提交完成,释放资源
如上图所示,参与者反馈1.3
后是处于阻塞状态等待2.1
,如果网络问题或协调者宕机了,接受不到2.1
,那么会一直阻塞数据不一致
:
两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能,比如在第二阶段中,假设协调者发出了事务commit
的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit
操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性
如上图所示,部分参与者接收不到2.1
,阻塞中,而接受到的就进行提交或回滚了,造成数据不一致
2.2 三阶段提交(3PC)
2.2.1 询问阶段(CanCommit)
预判断:协调者向参与者发送 CanCommit
请求,参与者如果可以提交就返回Yes响应,否则返回No响应
如上图所示,1.2
反馈情况
- 所有参与者都反馈可以
- 有参与者反馈不能成功执行(不管多少)
2.2.2 准备阶段(PreCommit)
准备提交:协调者根据参与者在询问阶段
的响应判断是否执行事务还是中断事务,参与者执行完操作之后返回ACK
响应,同时开始等待最终指令。
如上图所示
2.1
根据1.2
的情况- 所有参与者都反馈可以 –> 通知全部准备提交
- 有参与者反馈不能成功执行 –> 通知全部
abort
通知 - 等待反馈超时 –> 通知全部
abort
通知
协调者发起abort
通知后就会进入结束状态了,不再进行后续
2.2
根据2.1
- 通知全部准备提交 –> 执行事务,不提交
- 通知全部
abort
通知 –> 会中断事务的操作 - 等待超时 –> 会中断事务的操作
2.2.3 提交阶段(DoCommit)
提交阶段:协调者根据参与者在准备阶段
的响应判断是否执行事务还是中断事务:
- 如果所有参与者都返回正确的
ACK
响应,则提交事务 - 如果参与者有一个或多个参与者返回错误的
ACK
响应或者超时,则中断事务 - 如果参与者无法及时接收到来自协调者的提交或者中断事务请求时,在等待超时之后,会继续进行事务提交
如上图所示
3.1
根据2.3
的情况- 所有参与者都反馈体可以提交 –> 通知全部提交
- 有参与者反馈回滚或中断事务 –> 通知全部回滚
- 等待超时 –> 通知全部回滚
3.2
根据3.1
- 通知全部提交 –> 提交
- 通知全部回滚 –> 回滚
- 等待超时 –> 提交
为什么第三阶段等待超时就会自动提交呢?
因为经过了前面两阶段的判断,第三阶段可以提交的概率会大于回滚的概率
2.2.4 三阶段/二阶段差异
三阶段
的参与者是有超时机制的,等待请求超时会进行事务中断,或事务提交
而二阶段不会超时,只会阻塞
。
可以看出,三阶段提交解决的只是两阶段提交中单体故障和同步阻塞的问题,因为加入了超时机制,这里的超时的机制作用于 准备提交阶段
和 提交阶段
。如果等待 准备提交请求
超时,参与者直接回到准备阶段之前。如果等到提交请求超时,那参与者就会提交事务了
2PC
和 3PC
是分布式事务中两种常见的协议,3PC
可以看作是 2PC
协议的改进版本,相比于 2PC
它有两点改进:
- 引入了超时机制,同时在协调者和参与者中都引入超时机制(2PC 只有协调者有超时机制);
3PC
相比于2PC
增加了CanCommit
阶段,可以尽早的发现问题,从而避免了后续的阻塞和无效操作。
也就是说,3PC
相比于 2PC
,因为引入了超时机制,所以发生阻塞的几率变小了;同时 3PC
把之前 2PC
的准备阶段一分为二,变成了两步,这样就多了一个缓冲阶段,保证了在最后提交阶段之前各参与节点的状态是一致的
注意
:无论是2PC
还是3PC
都不能保证分布式系统中的数据100%
一致,与 2PC
协议相比,3PC
协议仍然可能存在阻塞的问题。
2.3 补偿提交(TCC)
2.3.1 定义
TCC(Try Confirm Cancel)
,是两阶段提交的一个变种,针对每个操作,都需要有一个其对应的确认
和取消
操作,当操作成功时调用确认操作,当操作失败时调用取消操作,类似于二阶段提交,只不过是这里的提交
和回滚
是针对业务上的,所以基于TCC
实现的分布式事务也可以看做是对业务的一种补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。
TCC(Try-Confirm-Cancel)
包括三段流程:
try阶段
:尝试去执行,完成所有业务的一致性检查,预留必须的业务资源。Confirm阶段
:确认执行业务,如果Try阶段
执行成功,接着执行Confirm 阶段
。该阶段对业务进行确认提交,不做任何检查,因为try阶段
已经检查过了,默认Confirm阶段
是不会出错的。Cancel 阶段
:取消待执行的业务,如果Try阶段
执行失败,执行Cancel 阶段
。进入该阶段会释放try阶段
占用的所有业务资源,并回滚Confirm阶段
执行的所有操作。
TCC
是业务层面的分布式事务,保证最终一致性,不会一直持有资源的锁。
优点
: 把数据库层的二阶段提交交给应用层来实现,规避了数据库的2PC
性能低下问题缺点
:TCC
的Try
、Confirm
和Cancel
操作功能需业务提供,开发成本高。TCC
对业务的侵入较大和业务紧耦合,需要根据特定的场景和业务逻辑来设计相应的操作
2.3.2 操作
数据库表需要存多两个字段可更新数
和 冻结数
,将各个服务的事务执行分成3个步骤
T
(try
,尝试更新阶段)- 原数据不变,只更新【可更新数】,同时保存变化差值在【冻结数】,方便回滚
- 状态设置为【更新中】
C
(confirm
,确认阶段)- 更新原数据,【可更新数】不变(在try阶段更新好了),清除【冻结数】
- 状态设置为【更新完】
此时表示业务正常完成了
C
(cancel
,补偿还原阶段)
如果try出错了,那各个服务就执行cancel还原数据,相当于回滚- 根据【可更新数】【冻结数】更新数据为事务前的样子
- 状态设置为更新前的状态【未更新】
如果是 confirm
或cancel
出错了,一般会去重复执行,因为过了try
,可以认为confirm
是一定可以执行成功的,除非重复执行次数达到阈值,就落地成日志,让人工处理
注意
:这里事务回滚的方式不像我们认为的那样 — 数据库直接给我们处理好
而是通过cancel
将数据补回去,所以TCC
也叫补偿机制
2.4 分布式事务总结
二阶段提交
、三阶段提交
、TCC
分别是三种实现分布式事务的方案
至于具体的实现框架:二阶段有mysql的XA事务,TCC有seata、tcc-transaction
我们看TCC会涉及到服务与服务之间的接口调用,因为网络问题,极有可能出现重复调用的情况,所以【confirm】【cancel】这些接口应该要实现幂等
当然服务间的通信除了通过【同步的rpc】,也可以通过【异步的MQ】来实现,所以引出了接下来的基于MQ最终一致性方案
3 分布式锁
3.1 Redis 分布式锁和Zookeeper 区别
3.1.1 二者使用方式
点击了解Zookeeper分布式锁
点击了解Redis分布式锁
3.1.2 二者区别
在功能上,Redis
的分布式锁和 Zookeeper
的分布式锁都能实现我们想要的功能,锁的互斥、重入等等。他们主要有以下几个区别:
- 性能区别
在性能方面,Redis
是基于内存存储的,而Zookeeper
是基于磁盘存储的,所以,在性能上,Redis
要比ZK
更好一些。 - 自动释放
Zookeeper
的锁的实现原理是基于客户端
和服务端
的连接来保证的,一旦连接断了,锁就会被自动释放。而Redis
的锁是需要自己主动加锁和解锁的,除非达到了超时时间,否则不会自动释放。
所以,Zookeeper
的分布式锁可以更好的应对客户端崩溃的情况,一旦客户端崩溃,锁就会释放,而Redis
实现的分布式锁,一旦客户端崩溃了,就没有人去进行释放了,只能等超时。
锁能自动释放有啥好处?除了提升并发度以外,还有个好处就是可以减少死锁发生的概率。因为锁释放了,所以就不会出现死锁了。 - 一致性&可用性要求(CAP)
我们知道Zookeeper
是一个CP
的系统,也就是他是保证强一致性的,而Redis
是一个AP
的系统,它是保证可用性的。
Zookeeper
会牺牲可用性来保证数据的一致性,即出现部分节点宕机后,集群中少于一半的节点后,或者集群正在进行master
选举时,都会拒绝新的写请求,导致无法加锁。
而Redis
会牺牲一致性性来保证可用性,即Redis
的集群中在做数据同步时,如果出现网络延迟,那么即使多个节点上面的数据不一样,客户端也可以正常的进行写入和读取。
那么,在使用Zookeeper
的分布式锁的时候,不会存在锁丢失的情况,也就是说不太会出现因为锁丢失而导致并发的情况。但是,可能会出现短暂的无法加锁的情况
。
而在使用Redis
的分布式锁的时候,除非集群都挂了,要不然不太会出现无法加锁的情况。但是可能会出现锁丢失的情况,或者说是重复加锁的情况,RedLock
的单点故障的问题。
3.1.3 选择使用
二者选择:
Redis
实现的分布式锁、性能更好,可用性更高。Zookeeper
实现的分布式锁可以自动释放,减少死锁出现的概率,并且他的一致性更有保障。- 如果分布式锁使用场景,对性能要求更高,可以牺牲一点一致性,那么就选择
Redis
的分布式锁。而如果场景对性能要求没那么高,但是对一致性要求非常高,那么则可以选择Zookeeper
其实,如果对可用性的要求高的话,用 Redis
也行,因为有个 RedLock,他的机制和 zk 很像,都是通过半数以上提交这种方式来避免因为单点问题而导致锁重复的。但是,RedLock 其实也不建议大家用,并且 Zookeeper
的分布式锁其实也不建议大家用。就直接用 Redis 就好了。
为啥呢?因为一般来说,我们在用分布式锁的时候,对性能要求肯定很高的,如果不高的话,直接用数据库的悲观锁就好了。没必要用分布式锁。
而且,往往我们在用分布式锁的时候,同时会伴随着幂等性判断、以及数据库兜底的唯一性约束的校验。所以,即使出现了极端情况,因为 Redis
的一致性没保证好,导致重复加锁了,我们也能在后续的环节中识别并防止并发。
而 Redis
的不可用的问题其实可以通过哨兵、集群等运维手段来解决的,所以,发生的概率本来就极低。所以说,日常开发的时候,只要我们把幂等判断、唯一性约束做好,对账最好,用 Redis
是最简单,高效的办法。
而且,Redis
作为一个缓存框架,很多应用都会直接依赖,直接用SETNX 或者 Redisson 加锁不要太方便。而 Zookeeper
,很多都是中间件在使用, 真正的业务应用依赖的很少的,多引入一个底层中间件,对系统来说也会提升复杂度,减少整体的稳定性的。
3.2 Springboot集成分布式锁
3.2.1 简介
Spring Integration
在基于Spring的应用程序中实现轻量级消息传递,并支持通过声明适配器与外部系统集成。Spring Integration
的主要目标是提供一个简单的模型来构建企业集成解决方案,同时保持关注点的分离,这对于生成可维护,可测试的代码至关重要。我们熟知的 Spring Cloud Stream
的底层就是Spring Integration
它们使用相同的API
抽象,这意味着,不论使用哪种存储,编码体验是一样的。试想一下你目前是基于zookeeper
实现的分布式锁,哪天想换成redis的实现,我们只需要修改相关依赖和配置就可以了,无需修改代码。下面是你使用 Spring Integration
实现分布式锁时需要关注的方法:
方法名 | 描述 |
---|---|
lock() | 加锁,如果已经被其他线程锁住或者当前线程不能获取锁则阻塞 |
lockInterruptibly() | 加锁,除非当前线程被打断。 |
tryLock() | 尝试加锁,如果已经有其他锁锁住,获取当前线程不能加锁,则返回false,加锁失败;加锁成功则返回true |
tryLock(long time, TimeUnit unit) | 尝试在指定时间内加锁,如果已经有其他锁锁住,获取当前线程不能加锁,则返回false,加锁失败;加锁成功则返回true |
unlock() | 解锁 |
3.2.2 基于Redis实现springboot集成
3.2.2.1 pom.xml和配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在application.yml中添加redis的配置
spring:
redis:
host: 172.31.0.149
port: 7111
3.2.2.2 配置类
建立配置类,注入RedisLockRegistry
@Configuration
public class RedisLockConfiguration {
@Bean
public RedisLockRegistry redisLockRegistry(RedisConnectionFactory redisConnectionFactory){
return new RedisLockRegistry(redisConnectionFactory, "redis-lock");
}
}
3.2.2.3 测试类
编写测试代码
@RestController
@RequestMapping("lock")
@Log4j2
public class DistributedLockController {
@Autowired
private RedisLockRegistry redisLockRegistry;
@GetMapping("/redis")
public void test1() {
Lock lock = redisLockRegistry.obtain("redis");
try{
//尝试在指定时间内加锁,如果已经有其他锁锁住,获取当前线程不能加锁,则返回false,加锁失败;加锁成功则返回true
if(lock.tryLock(3, TimeUnit.SECONDS)){
log.info("lock is ready");
TimeUnit.SECONDS.sleep(5);
}
} catch (InterruptedException e) {
log.error("obtain lock error",e);
} finally {
lock.unlock();
}
}
}
测试:启动多个实例,分别访问/lock/redis 端点,一个正常秩序业务逻辑,另外一个实例访问出现如下错误
说明第二个实例没有拿到锁,证明了分布式锁的存在。
注意,如果使用新版Springboot
进行集成时需要使用Redis4版本,否则会出现下面的异常告警,主要是 unlock()
释放锁时使用了UNLINK
命令,这个需要Redis4版本才能支持。
2020-05-14 11:30:24,781 WARN RedisLockRegistry:339 - The UNLINK command has failed (not supported on the Redis server?); falling back to the regular DELETE command
org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException: ERR unknown command 'UNLINK'
3.2.3 基于Zookeeper实现springboot集成
3.2.3.1 pom.xml和配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-zookeeper</artifactId>
</dependency>
在application.yml
中添加zookeeper
的配置
zookeeper:
host: 172.31.0.43:2181
3.2.3.2 配置类
建立配置类,注入ZookeeperLockRegistry
@Configuration
public class ZookeeperLockConfiguration {
@Value("${zookeeper.host}")
private String zkUrl;
@Bean
public CuratorFrameworkFactoryBean curatorFrameworkFactoryBean(){
return new CuratorFrameworkFactoryBean(zkUrl);
}
@Bean
public ZookeeperLockRegistry zookeeperLockRegistry(CuratorFramework curatorFramework){
return new ZookeeperLockRegistry(curatorFramework,"/zookeeper-lock");
}
}
3.2.3.3 编写测试代码
@RestController
@RequestMapping("lock")
@Log4j2
public class DistributedLockController {
@Autowired
private ZookeeperLockRegistry zookeeperLockRegistry;
@GetMapping("/zookeeper")
public void test2() {
Lock lock = zookeeperLockRegistry.obtain("zookeeper");
try{
//尝试在指定时间内加锁,如果已经有其他锁锁住,获取当前线程不能加锁,则返回false,加锁失败;加锁成功则返回true
if(lock.tryLock(3, TimeUnit.SECONDS)){
log.info("lock is ready");
TimeUnit.SECONDS.sleep(5);
}
} catch (InterruptedException e) {
log.error("obtain lock error",e);
} finally {
lock.unlock();
}
}
}
测试:启动多个实例,分别访问/lock/zookeeper 端点,一个正常秩序业务逻辑,另外一个实例访问出现如下错误
说明第二个实例没有拿到锁,证明了分布式锁的存在
3.3 分布式锁框架 Lock4j
3.3.1 简介
Lock4j
是一个分布式锁组件,它提供了多种不同的支持以满足不同性能和环境的需求,基于Spring AOP
的声明式和编程式分布式锁,支持RedisTemplate、Redisson、Zookeeper
特性:
- 简单易用,功能强大,扩展性强。
- 支持
redission, redisTemplate, zookeeper
,可混用,支持扩展。
3.3.2 Pom依赖和配置
<!-- Lock4j -->
<!-- 若使用redisTemplate作为分布式锁底层,则需要引入 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>lock4j-redis-template-spring-boot-starter</artifactId>
<version>2.2.4</version>
</dependency>
<!-- 若使用redisson作为分布式锁底层,则需要引入 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>lock4j-redisson-spring-boot-starter</artifactId>
<version>2.2.4</version>
</dependency>
spring:
redis:
database: 0
# Redis服务器地址 写你的ip
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password:
# 连接池最大连接数(使用负值表示没有限制 类似于mysql的连接池
jedis:
pool:
max-active: 200
# 连接池最大阻塞等待时间(使用负值表示没有限制) 表示连接池的链接拿完了 现在去申请需要等待的时间
max-wait: -1
# 连接池中的最大空闲连接
max-idle: 10
# 连接池中的最小空闲连接
min-idle: 0
# 连接超时时间(毫秒) 去链接redis服务端
timeout: 6000
3.3.3 注解属性介绍
package com.baomidou.lock.annotation;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Lock4j {
String name() default "";
Class<? extends LockExecutor> executor() default LockExecutor.class;
String[] keys() default {""};
long expire() default -1L;
long acquireTimeout() default -1L;
boolean autoRelease() default true;
}
@Lock4j属性 | 说明 |
---|---|
name | 需要锁住的key名称 |
executor | 可以通过该参数设置自定义特定的执行器 |
keys | 需要锁住的 keys 名称,可以是多个 |
expire | 锁过期时间,主要是用来防止死锁 |
acquireTimeout | 可以理解为排队等待时长,超过这个时长就退出排队,并排除获取锁超时异常 |
autoRelease | 是否自动释放锁,默认是 true |
3.3.4 简单使用
@RestController
@RequestMapping("/mock")
public class MockController {
@GetMapping("/lockMethod")
@Lock4j(keys = {"#key"}, acquireTimeout = 1000, expire = 10000)
public Result lockMethod(@RequestParam String key) {
ThreadUtil.sleep(5000);
return Result.OK(key);
}
}
如果抢占不到锁,Lock4j会抛出com.baomidou.lock.exception.LockFailureException: request failed,please retry it.
异常,通过全局异常处理
3.3.5 高级使用
3.3.5.1 自定义执行器Exector
Lock4j
的执行器(Executor
)负责实际的加锁和解锁操作。通过自定义执行器,可以根据具体的需求来实现加锁和解锁的逻辑。通过自定义执行器,可以灵活地选择适合你业务场景的分布式锁方案,并与 Lock4j
进行集成。
import com.baomidou.lock.executor.AbstractLockExecutor;
import org.springframework.stereotype.Component;
/**
* 自定义分布式锁执行器
*/
@Component
public class CustomRedissonLockExecutor extends AbstractLockExecutor {
@Override
public Object acquire(String lockKey, String lockValue, long expire, long acquireTimeout) {
return null;
}
@Override
public boolean releaseLock(String key, String value, Object lockInstance) {
return false;
}
}
在注解上直接指定特定的执行器:@Lock4j(executor = CustomRedissonLockExecutor.class)
3.3.5.2 自定义分布式锁key生成器
分布式锁通常需要一个唯一的键(key
)来标识不同的锁。默认情况下,Lock4j
使用 DefaultLockKeyBuilder
来生成锁键,它使用了一些基本信息如锁名称、作用域和持有者来生成一个唯一且具有可读性的字符串作为锁键。
通过自定义分布式锁键生成器,可以根据实际需求来定制生成逻辑。例如,可以基于业务场景、资源类型等因素来生成更具有语义和可读性的锁键。自定义分布式锁键生成器可以提高代码可读性,并确保在不同场景下生成唯一且合适的分布式锁键
import com.baomidou.lock.DefaultLockKeyBuilder;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.stereotype.Component;
/**
* 自定义分布式锁key生成器
*/
@Component
public class CustomKeyBuilder extends DefaultLockKeyBuilder {
public CustomKeyBuilder(BeanFactory beanFactory) {
super(beanFactory);
}
}
3.3.5.3 自定义抢占锁失败执行策略
import com.baomidou.lock.LockFailureStrategy;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* 自定义抢占锁失败执行策略
*/
@Component
public class GrabLockFailureStrategy implements LockFailureStrategy {
@Override
public void onLockFailure(String key, Method method, Object[] arguments) {
}
}
默认的锁获取失败策略为:com.baomidou.lock.DefaultLockFailureStrategy.
3.3.5.4 手动加锁释放锁
@Service
public class LockServiceImpl implements LockService {
@Autowired
private LockTemplate lockTemplate;
@Override
public void lock(String resourceKey) {
LockInfo lock = lockTemplate.lock(resourceKey, 10000L, 2000L, CustomRedissonLockExecutor.class);
if (lock == null) {
// 获取不到锁
throw new FrameworkException("业务处理中,请稍后再试...");
}
// 获取锁成功,处理业务
try {
doBusiness();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lockTemplate.releaseLock(lock);
}
}
private void doBusiness() {
// TODO 业务执行逻辑
}
}