分布式事务基础
事务的一致性
事务的执行使数据从一个状态转换为另一个状态,数据库的完整性约束没有被破坏。能量守恒,总量不变
例子:就转账来说,假设用户A和用户B两者的钱加起来一共是2000,那么不管A和B之间如何转账,转几次帐,事务结束后两个用户的钱相加起来应该还得是2000,这就是事务的一致性
事务的隔离性
事务的隔离性其实是要达到这么一个效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行
事务的持久性
事务的持久性指当事务正确完成后,它对于数据的改变是永久性的,不会轻易丢失
例如:我们在使用JDBC操作数据库时,在提交事务方法后,提示用户事务操作完成,当我们程序执行完成直到看到提示后,就可以认定事务已经正确提交,即使这时候数据库出现了问题,也必须要将我们的事务完全执行完成,否则就会造成我们看到提示事务处理完毕,但是数据库因为故障而没有执行事务的重大错误
分布式事务
随着互联网的快速发展,软件系统由原来的单体应用转变为分布式应用,下图描述了单体应用向微服务的演变:分布式系统会把一个应用系统拆分为可独立部署的多个服务,因此需要服务与服务之间远程协作才能完成事务操作,这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务,例如用户注册送积分事务、创建订单减库存事务,银行转账事务等都是分布式事务
典型的场景就是微服务架构,微服务之间通过远程调用完成事务操作。比如:订单微服务和库存微服务,下单的同时订单微服务请求库存微服务减库存。简言之:跨JVM进程产生分布式事务
分布式事务实现
begin transaction; // 1.本地数据库操作:张三减少金额 // 2.远程调用:让李四增加金额 commit transaction;
可以设想,当远程调用让李四增加金额成功了,由于网络问题,远程调用并没有返回,此时本地事务提交失败就回滚了张三减少金额的操作,此时张三和李四的数据就不一致了。因此在分布式架构的基础上,传统数据库事务就无法使用了。张三和李四的账户不在一个数据库中甚至不在一个应用系统里,实现转账事务需要通过远程调用,由于网络问题就会导致分布式事务问题
分布式事务产生场景
1、跨JVM进程产生分布式事务
典型的场景就是微服务架构,微服务之间通过远程调用完成事务操作。比如:订单微服务和库存微服务,下单的同时订单微服务请求库存微服务减库存
2、跨数据库实例
单体系统访问多个数据库实例,当单体系统需要访问多个数据库(实例)时就会产生分布式事务。比如:用户信息和订单信息分别在两个MySQL实例存储,用户管理系统删除用户信息,需要分别删除用户信息及用户的订单信息,由于数据分布在不同的数据实例,需要通过不同的数据库链接去操作数据,此时产生分布式事务。
与本地事务不同的是,分布式系统之所以叫分布式,是因为提供服务的各个节点分布在不同机器上,相互之间通过网络交互。不能因为有一点网络问题就导致整个系统无法提供服务,网络因素成为了分布式事务的考量标准之一。
CAP原则
CAP原则,指的是在一个分布式系统中,不可能同时满足以下三点:
- 一致性(Consistency):副本最新,指强一致性,在写操作完成后开始的任何读操作都必须返回该值,或者后续写操作的结果
- 也就是说,在一致性系统中,一旦客户端将值写入任何一台服务器并获得响应,那么之后client从其他任何服务器读取的都是刚写入的数据。一致性保证了不管向哪台服务器写入数据,其他的服务器能实时同步数据
- 可用性(Availability):高可用,可用性是指,每次向未崩溃的节点发送请求,总能保证收到响应数据(允许不是最新数据)
- 分区容忍性
- 什么是分区?
- 在分布式系统中,不同的结点分布在不同的子网络中,由于一些特殊的原因,这些子节点之间出现了网络不同的状态,但他们的内部子网是正常的,从而导致了整个系统的环境被切分成了若干个孤立的区域,这就是分区
分区容忍性:能容忍网络分区,分布式系统在遇到任何网络分区故障的时候,仍然能 够对外提供满足一致性和可用性的服务,也就是说,服务器A和B发送给对方的任何消 息都是可以放弃的,也就是说A和B可能因为各种意外情况,导致无法成功进行同步, 分布式系统要能容忍这种情况。除非整个网络环境都发生了故障
容许节点G1/G2间传递消息的差错(延迟或丢失),而不影响系统继续运行
以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择
为什么只能在A和C之间做出取舍?
分布式系统中,必须满足CAP中的P,此时只能在C/A之间作出取舍
整个系统由两个节点配合组成,之间通过网络通信,当节点A进行更新数据库操作的时候,需要同时更新节点B的数据库(这是一个原子的操作)
下面这个系统怎么满足CAP呢?我们用反证法假设可以同时满足一致性、可用性、分区容错这三个特性,由于满足分区容错,可以切断A/B的连线,如下图:
- 若要保证一致性:则必须进行节点间数据同步,同步期间数据锁定,导致期间读取失败或超时,破坏了可用性
- 若要保证可用性:则不允许节点间同步期间锁定,这又破坏了一致性
所以,最多满足两个条件
组合 | 结果 |
CA | 满足一致性和可用,放弃分区容错,其实就是一个单体应用 |
CP | 满足一致性和分区容错,也就是,放弃可用。当系统被分区,为了保证一致性,必须放弃可用性,让服务停用 |
AP | 满足可用性和分区容错,当出现分区,同时为了保证可用性,必须让结点继续对外服务,这样必然导致失去一致性 |
如何权衡保C还是保A?
取舍
- 舍弃P(选择C/A):单点的传统关系型数据库DBMS(MySQL),但如果采用集群就必须考虑P了
- 舍弃A(选择C/P):是分布式系统要保证P,而且保证一致性(先同步,之后才可用),如ZooKeeper/Redis/MongoDB/HBase
- 舍弃C(选择A/P):是分布式系统要保证P,而且保证可用性,如CoachDB/Cassandra/DynamoDB
redis-cluster是AP,zookeeper写入强一致,读取是顺序一致性(即弱一致)
对于一个分布式系统来说,CAP三者中:
- P是基本要求,只能通过基础设施提升,无法通过降低C/A来提升
- 然后在C/A两者之间权衡
一个还不错的策略是:保证可用性和分区容错,舍弃强一致性,但保证最终一致性,比如一些高并发的站点(秒杀、淘宝、12306)。最终近似于兼顾了三个特性
一致性和BASE理论
一致性
一致性可分为强一致性与弱一致性。所谓强一致性,即复制是同步的,弱一致性,即复制是异步的
CAP回顾:
CAP理论告诉我们一个悲惨但不得不接受的事实——我们只能在C、A、P中选择两个条件。而对于业务系统而言,我们往往选择牺牲一致性来换取系统的可用性和分区容错性。不过这里要指出的是,所谓的牺牲一致性并不是完全放弃数据一致性,而是牺牲强一致性换取弱一致性
强一致性:
系统中的某个数据被成功更新后,后续任何对该数据的读取操作都将得到更新后的值
两个要求:
- 任何一次读都能读到某个数据的最近一次写的数据
- 系统中的所有进程,看到的操作顺序,都和全局时钟下的顺序一致
简言之,在任意时刻,所有节点中的数据是一样的。例如,对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性
总结:
- 一个集群需要对外部提供强一致性,所以只要集群内部某一台服务器的数据发生了改变,那么就需要等待集群内其他服务器的数据同步完成后,才能正常地对外提供服务
- 保证了强一致性,务必会损耗可用性
弱一致性:
系统中的某个数据被更新后,后续对该数据的读取操作可能得到更新后的值,也可能是更新前的值
但即使过了“不一致时间窗口”这段时间后,后续对该数据的读取也不一定是最新值
所以说,可以理解为数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性
最终一致性:
是弱一致性的特殊形式,存储系统保证在没有新的更新的条件下,最终所有的访问都是最后更新的值
不保证在任意时刻任意节点上的同一份数据都是相同的,但是随着时间的迁移,不同节点上的同一份数据总是在向趋同的方向变化
简单说,就是在一段时间后,节点间的数据会最终达到一致状态
弱一致性与最终一致性的区别:
弱一致性即使过了不一致时间窗口,后续的读取也不一定能保证一致,而最终一致过了不一致窗口后,后续的读取一定一致
BASE理论
- BA:Basic Available(基本可用)
- 整个系统在某些不可抗力的情况下,仍然能够保证“可用性”,即一定时间内仍然能够返回一个明确的结果。只不过“基本可用”和“高可用”的区别是:
- “一定时间”可以适当延长,当举行大促时,响应时间可以适当延长(例如,秒杀时用MQ,慢慢消费)
- 给部分用户返回一个降级页面,直接返回一个降级页面,从而缓解服务器压力。但要注意,返回降级页面仍然是返回明确结果(例如,返回拥挤页面,比如一个商品秒杀,只有10个商品,就算来了1000个人,也没有意义,也就前面几十个人可能存在成功的情况,那么后面的人都返回降级页面)
- S:Soft State(柔性状态)
- 是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统不同节点的数据副本之间进行数据同步的过程存在延时(数据有一个字段,保存其中间状态,例如同步中,等待同步等等,这就是柔性状态)
- E:Eventual Consisstency(最终一致性)
- 同一数据的不同副本的状态,可以不需要实时一致,但一定要保证经过一定时间后仍然是一致的
BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性
分布式事务协议
背景:
在分布式系统中,每个节点都可以知道自己操作的成功或失败,却无法知道其他节点操作的成功或失败。当一个事务跨多个节点时,为了保证事务的原子性与一致性,而引入一个协调者来统一掌控所有参与者的操作结果,并指示它们是否要把操作结果进行真正的提交或者回滚
二阶段提交(2PC)
二阶段提交协议(Two-phase Commit,即2PC)是常用的分布式事务解决方案,即将事务的提交过程分为两个阶段来进行处理
阶段:
- 准备阶段
- 提交阶段
参与角色:
- 协调者:事务的发起者
- 参与者:事务的执行者
第一阶段(voting phase投票阶段)
1、协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待答复
2、各参与者执行事务操作,将undo和redo信息记入事务日志中(但不提交事务)
3、如参与者执行成功,给协调者反馈同意,否则反馈终止
第二阶段(commit phase提交执行阶段)
当协调者节点从所有参与者节点获得的相应消息都为同意时:
1、协调者节点向所有参与者节点发出“正式提交(commit)”的请求
2、参与者节点正式完成操作,并释放在整个事务期间内占用的资源
3、参与者节点向协调者节点发送Ack完成消息
4、协调者节点收到所有参与者节点反馈的ack完成消息后,完成事务
如果任一参与者节点在第一阶段返回的响应消息为中止(也就是返回的是不同意),或者协调者节点在第一阶段的访问超时之前无法获取所有参与者节点的响应消息时:
1、协调者节点向所有参与者节点发出回滚操作的请求
2、参与者节点利用阶段1写入的undo信息执行回滚,并释放在整个事务期间内占用的资源
3、参与者节点向协调者节点发送ack回滚完成消息
4、协调者节点受到所有参与者节点反馈的ack回滚完成消息后,取消事务
不管最后结果如何,第二阶段都会结束当前事务(不管是成功还是失败,在第二阶段都会完结这个事务)。
两阶段提交流程图:
两阶段案例
学校运动会上,100米决赛正准备开始,裁判对3个人分别询问
裁判:张三同学你准备好了吗?准备好了进第一赛道
张三:准备好了,随即进入第一赛道做好冲击姿势
裁判:李四同学你准备好了吗?准备好了进第二赛道
裁判:王五同学你准备好了吗?准备好了进第三赛道
王五:准备好了,.....
李四:准备好了,.....
...
如果有人没准备好,不同意,则裁判下达回滚指令
如果裁判收到了所有人的OK回复后,再次下令
裁判:跑...
...
张三、李四、执行完毕到达终点,汇报给了裁判
王五冲刺失败,汇报给了裁判
二阶段提交看起来确实能够提供原子性的操作,但是不幸的是,二阶段提交还是有几个缺点的:
1、性能问题:执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态
2、可靠性问题:参与者发生故障,协调者需要给每个参与者额外指定超时机制,超时后整个事务失效,协调者发生故障,参与者会一直阻塞下去,需要额外的备机进行容错
3、 数据的一致性问题:二阶段无法解决的问题:协调者在发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了,那么即使通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否已经提交
优点:
尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域(其实也不能100%保证强一致)
缺点:
实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景
3PC
三阶段提交协议,是二阶段提交协议的改进版本,三阶段提交有两个改动点
- 在协调者和参与者中都引入超时机制
- 在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的
也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样3阶段提交就有CanCommit、PreCommit、DoCommit三个阶段
小例子
班长要组织全班同学聚餐,由于大家毕业多年,所以要逐个打电话敲定时间,时间初定10.1日。然后开始逐个打电话。
班长:小A,我们想定在10.1号聚会,你有时间嘛?有时间你就说YES,没有你就说NO,然后我还会再去问其他人,具体时间地点我会再通知你,这段时间你可先去干你自己的事儿,不用一直等着我。(协调者询问事务是否可以执行,这一步不会锁定资源)
小A:好的,我有时间。(参与者反馈)
班长:小B,我们想定在10.1号聚会……不用一直等我。
班长收集完大家的时间情况了,一看大家都有时间,那么就再次通知大家。(协调者接收到所有YES指令)
班长:小A,我们确定了10.1号聚餐,你要把这一天的时间空出来,这一天你不能再安排其他的事儿了。然后我会逐个通知其他同学,通知完之后我会再来和你确认一下,还有啊,如果我没有特意给你打电话,你就10.1号那天来聚餐就行了。对了,你确定能来是吧?(协调者发送事务执行指令,这一步锁住资源。如果由于网络原因参与者在后面没有收到协调者的命令,他也会执行commit)
小A顺手在自己的日历上把10.1号这一天圈上了,然后跟班长说,我可以去。(参与者执行事务操作,反馈状态)
班长:小B,我们觉得了10.1号聚餐……你就10.1号那天来聚餐就行了。
班长通知完一圈之后。所有同学都跟他说:”我已经把10.1号这天空出来了”。于是,他在10.1号这一天又挨个打了一遍电话告诉他们:嘿,现在你们可以出门拉。。。。(协调者收到所有参与者的ACK响应,通知所有参与者执行事务的commit)
小A,小B:我已经出门拉。(执行commit操作,反馈状态)
阶段一:CanCommit阶段
3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应
1、事务询问
协调者向所有参与者发出包含事务内容的CanCommit请求,询问是否可以提交事务,并等待所有参与者答复
2、响应反馈
参与者收到CanCommit请求后,如果认为可以执行事务操作,则反馈yes并进入预备状态,否则反馈No
阶段二:PreCommit阶段
协调者根据参与者的反应情况来决定是否可以进行事务的PreCommit操作。根据响应情况,有以下两种可能
- 假如所有参与者均反馈yes,协调者预执行事务
1、发送预提交请求:协调者向参与者发送PreCommit请求,并进入准备阶段
2、事务预提交:参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中(但不提交事务)
3、响应反馈:如果参与者成功地执行了事务操作,则返回ACK响应,同时开始等待最终指令
- 假如有任何一个参与者向协调者发送了No指令,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断
1、发送中断请求:协调者向所有参与者发送abort请求
2、中断事务:参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断
阶段三:DoCommit阶段
该阶段进行真正的事务提交
注意:进入阶段3后,无论协调者出现问题,或者协调者与参与者网络出现问题,都会导致参与者无法接收到协调者发出的doCommit请求或abort请求。此时,参与者都会在等待超时之后,继续执行事务提交(也就是说,进入第三阶段后,参与者总是会尝试提交事务,虽然可能会被中断,这可能导致不一致问题)
DoCommit阶段也分为以下两种情况:
- 执行提交
所有参与者均反馈ack响应,执行真正的事务提交
1、发送提交请求
协调者接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求
2、事务提交
参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源
3、响应反馈
事务提交完之后,向协调者发送ack响应
4、完成事务
协调者接收到所有参与者的ack响应后,完成事务
- 中断事务
任何一个参与者反馈No,或者等待超时后协调者尚无法接收到所有参与者的反馈,即中断事务
1、发送中断请求
如果协调者处于工作状态,向所有参与者发出abort请求
2、事务回滚
参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源
3、反馈结果
参与者完成事务回滚之后,向协调者反馈ACK消息
4、中断事务
协调者接收到参与者反馈的ACK消息之后,执行事务的中断
注意
在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者abort请求时,在等待超时之后,继续进行事务的提交(当进入第三阶段时,说明参与者在第二阶段已经收到了PreCommit请求,那么协调者产生PreCommit请求的前提条件是它在第二阶段开始之前,收到了所有参与者的CanCommit响应都是Yes。(一旦参与者收到了PreCommit,意味着它知道大家其实都同意修改了),所以,当进入第三阶段时,由于网络超时等原因,虽然参与者没有收到commit或者abort响应,但是它有理由相信:成功提交的几率很大
这种情况可能会导致不一致性
3PC总结
优点:
相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题,阶段3中协调者出现问题时,参与者会继续提交事务
缺点:
数据不一致问题依然存在,当在参与者收到PreCommit请求后等待do commit指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致
分布式事务解决方案
- TCC
- 全局消息
- 基于可靠消息服务的分布式事务
- 最大努力通知
TCC(事务补偿)
TCC方案是一种应用层面侵入业务的两阶段提交,是目前最火的一种柔性事务方案,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作
1、第一阶段
Try:主要是对业务系统做检测及资源预留(加锁,锁住资源)
2、第二阶段
本阶段根据第一阶段的结果,决定是执行confirm还是cancel
Confirm(确认):执行真正的业务(执行业务,之后释放锁)
Cancel(取消):是预留资源的取消(出问题,释放锁)
案例
为了方便理解,下面以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建2个步骤,库存服务和订单服务分别在不同的服务器节点上
假设商品库存为100,购买数量为2,这里检查和更新库存的同时,冻结用户购买数量的库存,同时创建订单,订单状态为待确认
1、Try阶段
TCC机制中的Try仅是一个初步操作,它和后续的确认一起才能真正构成一个完整的业务逻辑,这个阶段主要完成:
- 完成所有业务检查(一致性)
- 预留必须业务资源(准隔离性)
- Try尝试执行业务
2、Confirm/Cancel阶段
根据Try阶段服务是否全部正常执行,继续执行确认操作(Confirm)或取消操作(Cancel)
Confirm和Cancel操作满足幂等性,如果Confirm或Cancel操作执行失败,将会不断重试直到执行完成
Confirm:当Try阶段服务全部正常执行,执行确认业务逻辑操作
这里使用的资源一定是Try阶段预留的业务资源。在TCC事务机制中认为,如果在Try阶段能正常地预留资源,那Confirm一定能完整正确地提交
Confirm阶段也可以看成是对Try阶段的一个补充,Try-Confirm一起组成了一个完整的业务逻辑
Cancel:当Try阶段存在服务执行失败,进入Cancel阶段
Cancel取消执行,释放Try阶段预留的业务资源,上面的例子中,Cancel操作会把冻结的库存释放,并更新订单状态为取消
最终一致性保证
- TCC事务机制以Try操作为中心,Confirm和Cancel操作都是围绕Try而展开。因此,Try阶段中的操作,其保障性是最好的,即使失败,仍然有取消操作(Cancel)可以将其执行结果撤销
- Try阶段执行成功并开始执行Confirm阶段时,默认Confirm阶段是不会出错的。也就是说只要Try成功,Confirm一定成功(TCC设计之初的定义)
- Confirm与Cancel如果失败,由TCC框架进行重试补偿
- 存在极低概率在CC环节彻底失败,则需要定时任务或人工介入
方案总结
TCC事务机制相对于传统事务机制(X/Open XA),TCC事务机制有以下优点:
- 性能提升:具体业务来实现控制资源锁的粒度变小,不会锁定整个资源
- 数据最终一致性:基于Confirm和Cancel的幂等性,保证事务最终完成确认或者取消,保证数据的一致性
- 可靠性:解决了XA协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群
缺点:TCC的Try、Confirm和Cancel操作功能都要按具体业务来实现,业务耦合度较高,提高了开发成本
ByteTCC
https://github.com/liuyangming/ByteTCC
Seata
Seata基础
https://seata.io/zh-cn/index.html
Seata为用户提供AT、TCC、SAGA、XA事务模式
Seata术语
TC(Transaction Coordinator)事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚(协调事务的状态)
TM(Transaction Manager)事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务(可以理解为业务中台,通过rpc调用其他微服务的事务)
RM(Resource Manager)资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚(子服务的事务)
UNDO_LOG表:(回滚日志)
1、UNDO_LOG必须在每个业务数据库中创建,用于保存回滚操作数据
2、当全局提交时,UNDO_LOG记录直接删除
3、当全局回滚时,将现有数据撤销,还原至操作前的状态
CREATETABLE `undo_log` ( `id` bigint(20)NOTNULL AUTO_INCREMENT, `branch_id` bigint(20)NOTNULL, `xid` varchar(100)NOTNULL, `context` varchar(128)NOTNULL, `rollback_info` longblobNOTNULL, `log_status` int(11)NOTNULL, `log_created` datetimeNOTNULL, `log_modified` datetimeNOTNULL, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
branch_id:branch指的是每个服务,即每个本地事务(分支事务)
AT模式
AT模式运行机制
AT模式的特点就是对业务无入侵式,整体机制分为二阶段提交
- 两阶段提交协议的演变:
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源
- 二阶段:
- 提交异步化,非常快速地完成
- 回滚通过一阶段的回滚日志进行反向补偿
在AT模式下,用户只需关注自己的业务SQL,用户的业务SQL作为一阶段,Seata框架会自动生成事务的二阶段提交和回滚操作
Seata具体实现步骤:
1、TM端使用@GlobalTransaction进行全局事务开启、提交、回滚
2、TM开始RPC调用远程服务
3、RM端seata-client通过扩展DataSourceProxy,实现自动生成UNDO_LOG与TC上报
4、TM告知TC提交/回滚全局事务
搭建Seata TC协调者
seata 1.3.0
用seata库作协调者TC的库:
-- the table to store GlobalSession datadroptable if exists `global_table`;createtable `global_table` ( `xid` varchar(128)notnull, `transaction_id` bigint, `status` tinyintnotnull, `application_id` varchar(32), `transaction_service_group` varchar(32), `transaction_name` varchar(128), `timeout` int, `begin_time` bigint, `application_data` varchar(2000), `gmt_create` datetime, `gmt_modified` datetime, primary key (`xid`), key `idx_gmt_modified_status` (`gmt_modified`, `status`), key `idx_transaction_id` (`transaction_id`));-- the table to store BranchSession datadroptable if exists `branch_table`;createtable `branch_table` ( `branch_id` bigintnotnull, `xid` varchar(128)notnull, `transaction_id` bigint, `resource_group_id` varchar(32), `resource_id` varchar(256), `lock_key` varchar(128), `branch_type` varchar(8), `status` tinyint, `client_id` varchar(64), `application_data` varchar(2000), `gmt_create` datetime, `gmt_modified` datetime, primary key (`branch_id`), key `idx_xid` (`xid`));-- the table to store lock datadroptable if exists `lock_table`;createtable `lock_table` ( `row_key` varchar(128)notnull, `xid` varchar(96), `transaction_id` long, `branch_id` long, `resource_id` varchar(256), `table_name` varchar(32), `pk` varchar(36), `gmt_create` datetime, `gmt_modified` datetime, primary key(`row_key`));-- the table to store seata xid data-- 0.7.0+ add context-- you must to init this sql for you business databese. the seata server not need it.-- 此脚本必须初始化在你当前的业务数据库中,用于AT 模式XID记录。与server端无关(注:业务数据库)-- 注意此处0.3.0+ 增加唯一索引 ux_undo_logdroptable `undo_log`;CREATETABLE `undo_log` ( `id` bigint(20)NOTNULL AUTO_INCREMENT, `branch_id` bigint(20)NOTNULL, `xid` varchar(100)NOTNULL, `context` varchar(128)NOTNULL, `rollback_info` longblobNOTNULL, `log_status` int(11)NOTNULL, `log_created` datetimeNOTNULL, `log_modified` datetimeNOTNULL, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
使用Nacos作为Seata的配置中心
编写AT模式代码
RM实现
1、创建订单和库存服务的DB
-- 库存服务DB执行CREATETABLE `tab_storage` ( `id` bigint(11)NOTNULL AUTO_INCREMENT, `product_id` bigint(11) DEFAULT NULL COMMENT '产品id', `total` int(11) DEFAULT NULL COMMENT '总库存', `used` int(11) DEFAULT NULL COMMENT '已用库存', PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;INSERTINTO `tab_storage` (`product_id`, `total`,`used`)VALUES('1','96','4');INSERTINTO `tab_storage` (`product_id`, `total`,`used`)VALUES('2','100','0');-- 订单服务DB执行CREATETABLE `tab_order` ( `id` bigint(11)NOTNULL AUTO_INCREMENT, `user_id` bigint(11) DEFAULT NULL COMMENT '用户id', `product_id` bigint(11) DEFAULT NULL COMMENT '产品id', `count` int(11) DEFAULT NULL COMMENT '数量', `money` decimal(11,0) DEFAULT NULL COMMENT '金额', `status` int(1) DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完成', PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
2、各数据库中加入undo_log表
CREATETABLE `undo_log` ( `id` bigint(20)NOTNULL AUTO_INCREMENT, `branch_id` bigint(20)NOTNULL, `xid` varchar(100)NOTNULL, `context` varchar(128)NOTNULL, `rollback_info` longblobNOTNULL, `log_status` int(11)NOTNULL, `log_created` datetimeNOTNULL, `log_modified` datetimeNOTNULL, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
3、添加seata pom.xml依赖
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.2.RELEASE</version><relativePath/></parent> ...... <properties><springboot.verison>2.3.2.RELEASE</springboot.verison><java.version>1.8</java.version><mybatis.version>2.1.5</mybatis.version><tk-mapper.version>4.1.5</tk-mapper.version><seata.version>1.3.0</seata.version></properties> ...... <!--demo01父模块中添加依赖--><dependencyManagement><dependencies><!--Mybatis通用Mapper--><dependency><groupId>tk.mybatis</groupId><artifactId>mapper-spring-boot-starter</artifactId><version>${mybatis.version}</version></dependency><dependency><groupId>tk.mybatis</groupId><artifactId>mapper</artifactId><version>${tk-mapper.version}</version></dependency><!--SpringCloud--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>Hoxton.SR9</version><type>pom</type><scope>import</scope></dependency><!--Spring Alibaba Cloud--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>2.2.1.RELEASE</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><!--子模块order-service和storage-service的pom中添加nacos和seata依赖--><dependencies><!--nacos注册中心和配置中心--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency><!--seata--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId><exclusions><!--移除掉该starter中自带的依赖,该依赖版本较低--><exclusion><groupId>io.seata</groupId><artifactId>seata-spring-boot-starter</artifactId></exclusion></exclusions></dependency><!--单独添加seata 1.3.0的依赖--><dependency><groupId>io.seata</groupId><artifactId>seata-spring-boot-starter</artifactId><version>1.3.0</version></dependency><!--openfeign--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><dependency><groupId>io.github.openfeign</groupId><artifactId>feign-okhttp</artifactId><version>10.2.3</version></dependency><!--Mybatis通用Mapper--><dependency><groupId>tk.mybatis</groupId><artifactId>mapper-spring-boot-starter</artifactId></dependency><dependency><groupId>tk.mybatis</groupId><artifactId>mapper</artifactId></dependency><!--mysql--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency></dependencies>
4、yml配置
server port6770spring application name order-service datasource driver-class-name com.mysql.cj.jdbc.Driver username seata_test password'seata1234abcd!' url jdbc mysql //rm-bp17dq6iz79761b8fxo.mysql.rds.aliyuncs.com 3306/it235_order?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true cloud nacos discovery server-addr nacos.it235.com80 register-enabledtrue namespace f46bbdaa-f11e-414f-9530-e6a18cbf91f6 config server-addr nacos.it235.com80 enabledtrue file-extension yaml namespace f46bbdaa-f11e-414f-9530-e6a18cbf91f6 seata enabledtrue application-id $ spring.application.name# 事务群组(可以每个应用独立取名,也可以使用相同的名字),要与服务端nacos-config.txt中service.vgroup_mapping的后缀对应 tx-service-group $ spring.application.name -tx-group config type nacos # 需要和server在同一个注册中心下 nacos namespace f46bbdaa-f11e-414f-9530-e6a18cbf91f6 serverAddr nacos.it235.com80# 需要server端(registry和config)、nacos配置client端(registry和config)保持一致 group SEATA_GROUP username"nacos" password"nacos" registry type nacos nacos# 需要和server端保持一致,即server在nacos中的名称,默认为seata-server application seata-server server-addr nacos.it235.com80 group SEATA_GROUP namespace f46bbdaa-f11e-414f-9530-e6a18cbf91f6 username"nacos" password"nacos"mybatis mapperLocations classpath mapper/*.xml
5、编写业务代码
packagecom.it235.seata.order; importtk.mybatis.spring.annotation.MapperScan; "com.it235.seata.order.mapper") (publicclassOrderServiceApplication { privateOrderServiceorderService; "order/create") (publicBooleancreate(longuserId , longproductId){ Orderorder=newOrder(); order.setCount(1) .setMoney(BigDecimal.valueOf(88)) .setProductId(productId) .setUserId(userId) .setStatus(0); returnorderService.create(order); } publicstaticvoidmain(String[] args) { SpringApplication.run(OrderServiceApplication.class, args); } } publicclassOrderServiceImplimplementsOrderService { privateOrderMapperorderMapper; publicbooleancreate(Orderorder) { log.info("创建订单开始"); intindex=orderMapper.insert(order); log.info("创建订单结束"); returnindex>0; } } name="tab_order") (chain=true) (publicclassOrder { strategy=GenerationType.IDENTITY) (privateLongid; privateLonguserId; privateLongproductId; privateintcount; privateBigDecimalmoney; privateintstatus; } publicinterfaceOrderMapperextendsMapper<Order> { } packagecom.xiaolingbao.orderservice.service; importcom.xiaolingbao.orderservice.model.Order; /*** @author: xiaolingbao* @date: 2023/5/8 21:25* @description: */publicinterfaceOrderService { booleancreate(Orderorder); }
6、浏览器访问模拟添加订单请求,http://localhost:6770/order/create,查看数据库,此时单个服务搭建完成
7、依葫芦画瓢。搭建库存服务,同时保证库存服务正常启动注册
8、注意nacos中需要存在对应的service.vgroupMapping
TM实现
搭建business
服务,pom、bootstrap.yml
与RM基本一致,并提供FeignClient
调用组件
# feign组件超时设置,用于查看seata数据库中的临时数据内容feign client config default connect-timeout30000 read-timeout30000
FeignClient
组件代码编写
name="storage-service") (publicinterfaceStorageClient { "storage/change") (BooleanchangeStorage( ("productId") longproductId , ("used") intused); } name="order-service") (publicinterfaceOrderClient { "order/create") (Booleancreate( ("userId") longuserId , ("productId") longproductId); }
调用层代码编写
publicclassBusinessServiceApplication { privateOrderClientorderClient; privateStorageClientstorageClient; "buy") (publicStringbuy(longuserId , longproductId){ orderClient.create(userId , productId); storageClient.changeStorage(userId , 1); return"ok"; } publicstaticvoidmain(String[] args) { SpringApplication.run(BusinessServiceApplication.class, args); } }
AT模式原理解析
TC相关的表解析:
- global_table:全局事务,每当有一个全局事务发起后,就会在该表中记录全局事务的ID
- branch_table:分支事务,记录每一个分支事务的ID,分支事务操作的哪个数据库等信息
- lock_table:全局锁
日志分析:
1、UNDO_LOG日志分析
{ "@class": "io.seata.rm.datasource.undo.BranchUndoLog", "xid": "192.168.2.196:8091:104983180048351232", "branchId": 104983207323910145, "sqlUndoLogs": ["java.util.ArrayList", [{ "@class": "io.seata.rm.datasource.undo.SQLUndoLog", "sqlType": "UPDATE", "tableName": "tab_storage", "beforeImage": { "@class": "io.seata.rm.datasource.sql.struct.TableRecords", "tableName": "tab_storage", "rows": ["java.util.ArrayList", [{ "@class": "io.seata.rm.datasource.sql.struct.Row", "fields": ["java.util.ArrayList", [{ "@class": "io.seata.rm.datasource.sql.struct.Field", "name": "id", "keyType": "PRIMARY_KEY", "type": -5, "value": ["java.lang.Long", 1] }, { "@class": "io.seata.rm.datasource.sql.struct.Field", "name": "total", "keyType": "NULL", "type": 4, "value": 88 }, { "@class": "io.seata.rm.datasource.sql.struct.Field", "name": "used", "keyType": "NULL", "type": 4, "value": 12 }]] }]] }, "afterImage": { "@class": "io.seata.rm.datasource.sql.struct.TableRecords", "tableName": "tab_storage", "rows": ["java.util.ArrayList", [{ "@class": "io.seata.rm.datasource.sql.struct.Row", "fields": ["java.util.ArrayList", [{ "@class": "io.seata.rm.datasource.sql.struct.Field", "name": "id", "keyType": "PRIMARY_KEY", "type": -5, "value": ["java.lang.Long", 1] }, { "@class": "io.seata.rm.datasource.sql.struct.Field", "name": "total", "keyType": "NULL", "type": 4, "value": 87 }, { "@class": "io.seata.rm.datasource.sql.struct.Field", "name": "used", "keyType": "NULL", "type": 4, "value": 13 }]] }]] } }]] } { "@class": "io.seata.rm.datasource.undo.BranchUndoLog", "xid": "192.168.2.196:8091:104983180048351232", "branchId": 104983197731536896, "sqlUndoLogs": ["java.util.ArrayList", [{ "@class": "io.seata.rm.datasource.undo.SQLUndoLog", "sqlType": "INSERT", "tableName": "tab_order", "beforeImage": { "@class": "io.seata.rm.datasource.sql.struct.TableRecords$EmptyTableRecords", "tableName": "tab_order", "rows": ["java.util.ArrayList", []] }, "afterImage": { "@class": "io.seata.rm.datasource.sql.struct.TableRecords", "tableName": "tab_order", "rows": ["java.util.ArrayList", [{ "@class": "io.seata.rm.datasource.sql.struct.Row", "fields": ["java.util.ArrayList", [{ "@class": "io.seata.rm.datasource.sql.struct.Field", "name": "id", "keyType": "PRIMARY_KEY", "type": -5, "value": ["java.lang.Long", 18] }, { "@class": "io.seata.rm.datasource.sql.struct.Field", "name": "user_id", "keyType": "NULL", "type": -5, "value": ["java.lang.Long", 1] }, { "@class": "io.seata.rm.datasource.sql.struct.Field", "name": "product_id", "keyType": "NULL", "type": -5, "value": ["java.lang.Long", 1] }, { "@class": "io.seata.rm.datasource.sql.struct.Field", "name": "count", "keyType": "NULL", "type": 4, "value": null }, { "@class": "io.seata.rm.datasource.sql.struct.Field", "name": "money", "keyType": "NULL", "type": 3, "value": ["java.math.BigDecimal", 88] }, { "@class": "io.seata.rm.datasource.sql.struct.Field", "name": "status", "keyType": "NULL", "type": 4, "value": null }]] }]] } }]] }
2、系统日志分析(TC的日志)
2021-02-1616:45:40.728INFO--- [ServerHandlerThread_1_4_500] i.s.s.coordinator.DefaultCoordinator : BeginnewglobaltransactionapplicationId: business-service,transactionServiceGroup: business-service-tx-group, transactionName: buy(long, long),timeout:60000,xid:192.168.2.196:8091:1049831800483512322021-02-1616:45:44.714INFO--- [batchLoggerPrint_1_1] i.s.c.r.p.server.BatchLogHandler : xid=192.168.2.196:8091:104983180048351232,branchType=AT,resourceId=jdbc:mysql://rm-bp17dq6iz79761b8fxo.mysql.rds.aliyuncs.com:3306/it235_order,lockKey=tab_order:18,clientIp:192.168.2.196,vgroup:order-service-tx-group2021-02-1616:45:44.935INFO--- [ServerHandlerThread_1_5_500] i.seata.server.coordinator.AbstractCore : Registerbranchsuccessfully, xid=192.168.2.196:8091:104983180048351232, branchId=104983197731536896, resourceId=jdbc:mysql://rm-bp17dq6iz79761b8fxo.mysql.rds.aliyuncs.com:3306/it235_order ,lockKeys = tab_order:182021-02-1616:46:40.917INFO--- [TxTimeoutCheck_1_1] i.s.s.coordinator.DefaultCoordinator : Globaltransaction[192.168.2.196:8091:104983180048351232] istimeoutandwillberollback. 2021-02-1616:46:42.280INFO--- [RetryRollbacking_1_1] io.seata.server.coordinator.DefaultCore : Rollbackbranchtransactionsuccessfully, xid=192.168.2.196:8091:104983180048351232branchId=1049832073239101452021-02-1616:46:42.658INFO--- [RetryRollbacking_1_1] io.seata.server.coordinator.DefaultCore : Rollbackbranchtransactionsuccessfully, xid=192.168.2.196:8091:104983180048351232branchId=1049831977315368962021-02-1616:46:42.720INFO--- [RetryRollbacking_1_1] io.seata.server.coordinator.DefaultCore : Rollbackglobaltransactionsuccessfully, xid=192.168.2.196:8091:104983180048351232.
原理
AT优势:对业务无侵入
一阶段步骤:
TM:business-service.buy(long, long)方法执行时,由于该方法具有@GlobalTransactional标志,该TM会向TC发起全局事务,生成XID(全局锁)
- RM:OrderService.create(long, long):写表,UNDO_LOG记录回滚日志(Branch ID),通知TC操作结果
- RM:StorageService.changeNum(long, long):写表,UNDO_LOG记录回滚日志(Branch ID),通知TC操作结果
RM写表的过程,seata会拦截业务SQL,首先解析SQL语义,在业务数据被更新前,将其保存成before image,然后执行业务SQL,在业务数据更新之后,再将其保存成after image,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性
二阶段步骤:
因为业务SQL在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可
- 正常:TM执行成功,通知TC全局提交,TC此时通知所有的RM提交成功,删除UNDO_LOG回滚日志
- 异常:TM执行失败,通知TC全局回滚,TC此时通知所有的RM进行回滚,根据UNDO_LOG反向操作,使用before image还原业务数据,删除UNDO_LOG,但在还原前要首先校验脏写,对比数据库当前业务数据和after image,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理
AT模式的一阶段、二阶段提交和回滚均有Seata框架自动生成,用户只需编写业务SQL,便能轻松接入分布式事务,AT模式是一种对业务无任何侵入的分布式事务解决方案
读写隔离
1、写隔离
- 一阶段本地事务提交前,需要确保先拿到全局锁
- 拿不到全局锁,不能提交本地事务
- 拿全局锁的尝试被限制在一定范围内(如时间限制),超出范围将放弃,并回滚本地事务,释放本地锁
以一个示例来说明:
两个全局事务tx1和tx2,分别对a表的m字段进行更新操作,m的初始值1000
tx1先开始,开启本地事务,拿到本地锁,更新操作m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的全局锁(不拿到全局锁,是不具备提交本地事务的权限的),本地提交释放本地锁。tx2后开始,开启本地事务,拿到本地锁,更新操作m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的全局锁,tx1全局提交前,该记录的全局锁被tx1持有,tx2需要重试等待全局锁
tx1二阶段全局提交,释放全局锁,tx2拿到全局锁提交本地事务
如果tx1的二阶段全局回滚,则tx1需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚
此时,如果tx2仍在等待该数据的全局锁,同时持有本地锁,则tx1的分支回滚会失败。分支的回滚会一直重试,直到tx2的全局锁等待超时,放弃全局锁并回滚本地事务释放本地锁,tx1的分支回滚最终成功
因为整个过程全局锁在tx1结束前一直是被tx1持有的,所以不会发生脏写的问题
2、读隔离
在数据库本地事务隔离级别读已提交(Read Committed)或以上的基础上,Seata的AT模式默认全局隔离级别是读未提交(Read Uncommitted)
如果应用在特定场景下,必须要求全局的读已提交,目前Seata的方式是通过select for update语句处理
select for update语句的执行会申请全局锁,如果全局锁被其他事务持有,则释放本地锁(回滚select for update语句的本地执行)并重试。这个过程中,查询是被block住的,直到全局锁拿到,即读取的相关数据是已提交的,才返回
出于总体性能上的考虑,Seata目前的方案并没有对所有select语句都进行处理,仅针对for update的select语句
for update扩展
1、使用场景
如果遇到存在高并发并且对于数据的准确性很有要求的场景,需要使用for update
比如涉及到金钱、库存等。一般这些操作都是很长一串并且是开启事务的。如果库存刚开始读的时候是1,而立马另一个进程进行了update将库存更新为0了,而事务还没有结束,会将错的数据一直执行下去,就会有问题。所以需要for update进行数据加锁防止高并发时候数据出错
- for update仅适用于InnoDB,并且必须开启事务,在begin与commit之间才生效
- 要测试for update的锁表情况,可以利用MySQL的Command Mode,开启二个视窗来做测试
2、窗口模拟
- 窗口A,非自动提交事务,用于for update操作
set autocommit =0;begin;select*from tab_order where id =1 for update;-- 等第二个窗口执行完成之后再执行commitcommit;
- 窗口B,用于普通update操作
在b窗口对ID=1的数据进行update name操作,发现失败:等待锁释放超时
update tab_order set product_id = 100 where id = 1;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
在对id = 2的数据进行update name操作,成功
update tab_order set product_id = 200 where id = 2;
Query OK, 1 row affected (0.00 sec)
3、总结
- for update操作在未获取到数据的时候,mysql不进行锁(no lock)
- 获取到数据的时候,进行对约束字段的判断,存在有索引的字段则进行row lock否则进行table lock
- 当使用'<>', 'like'等关键字时,进行for update操作时,mysql进行的是table lock
for update不是表锁也不是行锁,需要看情况
TCC模式详解
TCC模式需要用户根据自己的业务场景实现Try、Confirm和Cancel三个操作;事务发起方在一阶段执行Try方法,在二阶段执行Confirm方法,二阶段回滚执行Cancel方法
TCC三个方法描述:
- Try:资源的检测和预留
- Confirm:执行的业务操作提交;要求Try成功Confirm一定要能成功
- Cancel:预留资源释放
TCC的实践经验
蚂蚁TCC实践,总结以下注意事项:
业务模型分2阶段设计——>并发控制——>允许空回滚——>防悬挂控制——>幂等控制
TCC设计——业务模型分2阶段设计
用户接入TCC,最重要的是考虑如何将自己的业务模型拆成两阶段来实现
以扣钱场景为例,在接入TCC前,对A账户的扣钱,只需要一条更新账户余额的SQL便能完成;但是在接入TCC之后,用户就需要考虑如何将原来一步就能完成的扣钱操作,拆成两阶段,实现成三个方法,并且保证一阶段Try成功的话,二阶段Confirm一定成功
如上图所示,Try方法作为一阶段准备方法,需要做资源的检查和预留。在扣钱场景下,Try要做的事情就是检查账户余额是否充足,预留转账资金,预留的方式就是冻结A账户的转账资金。Try方法执行之后,账号A余额虽然还是100,但是其中30元已经被冻结了,不能被其他事务使用
二阶段Confirm方法执行真正的扣钱操作。Confirm会使用Try阶段冻结的资金,执行账号扣款。Confirm方法执行之后,账号A在一阶段中冻结的30元已经被扣除,账号A余额变成70元
如果二阶段是回滚的话,就需要在Cancel方法内释放一阶段Try冻结的30元,使账号A回到初始状态,100元全部可用
用户接入TCC模式,最重要的事情就是考虑如何将业务模型拆成2阶段,实现成TCC的3个方法,并且保证Try成功Confirm一定能成功。相对于AT模式,TCC模式对业务代码有一定的侵入性,但是TCC模式无AT模式的全局行锁,TCC性能会比AT模式高很多
TCC设计——允许空回滚
Cancel接口设计时需要允许空回滚。在Try接口因为丢包时没有收到,事务管理器会触发回滚,这是会触发Cancel接口,这时Cancel执行时发现没有对应的事务xid或主键时,需要返回回滚成功。让事务服务管理器认为已回滚,否则会不断重试,而Cancel又没有对应的业务数据可以进行回滚
TCC设计——防悬挂控制
悬挂的意思是:Cancel比Try接口先执行,出现的原因是Try由于网络拥堵而超时,事务管理器生成回滚,触发Cancel接口,而最终又收到了Try接口调用,但是Cancel比Try先到。按照前面允许空回滚的逻辑,回滚会返回成功,事务管理器认为事务已回滚成功,则此时的Try接口不应该执行,否则会产生数据不一致,所以我们在Cancel空回滚返回成功之前先记录该条事务xid或业务主键,标识这条记录已经回滚过,Try接口先检查这条事务xid或业务主键,如果已经标记为回滚成功过,则不执行Try的业务操作
TCC设计——幂等控制
幂等性的意思是:对同一个系统,使用同样的条件,一次请求和重复的多次请求对系统资源的影响是一致的。因为网络抖动或拥堵可能会超时,事务管理器会对资源进行重试操作,所以很可能一个业务操作会被重复调用,为了不因为重复调用而多次占用资源,需要对服务设计时进行幂等控制,通常我们可以用事务xid或业务主键判重来控制
Seata Saga模式详解
Saga模式
Saga是一种补偿协议,在Saga模式下,分布式事务内有多个参与者,每个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作
如图:T1~T3都是正向的业务流程,都对应着一个冲正逆向操作C1~C3
分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交。如果任何一个正向操作执行失败,那么分布式事务会退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态
Saga正向服务与补偿服务也需要业务开发者实现。因此是业务入侵的
Saga模式下分布式事务通常是由事件驱动的,各个参与者之间是异步执行的,Saga模式是一种长事务解决方案
Saga模式使用场景
Saga模式适用于业务流程长且需要保证事务最终一致性的业务系统,Saga模式一阶段就会提交本地事务,无锁、长流程情况下可以保证性能
事务参与者可能是其他公司的服务或者是遗留系统的服务,无法进行改造和提供TCC要求的接口,可以使用saga模式
saga模式的优势是:
- 一阶段提交本地数据库事务,无锁,高性能
- 参与者可以采用事务驱动异步执行,高吞吐
- 补偿服务即正向服务的反向,易于理解、实现
缺点:Saga模式由于一阶段已经提交本地数据库事务,且没有进行预留动作,所以不能保证隔离性。
与TCC相同,saga中,每个事务参与者的冲正、逆向操作都需要支持:
- 空补偿:当逆向操作早于正向操作时
- 防悬挂:空补偿后要拒绝正向操作
- 幂等
总结各种分布式模式