文章目录如下:
什么是分布式事务?
分布式对应的是单体架构,互联网早起单体架构是非常流行的,好像是一个家族企业,大家在一个家里劳作,单体架构如下图:
但是随着业务的复杂度提高,大家族人手不够,此时不得不招人,这样逐渐演变出了分布式服务,互相协作,每个服务负责不同的业务,架构如下图:
分布式架构
因此需要服务与服务之间的远程协作才能完成事务,这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务,例如用户注册送积分 事务、创建订单减库存事务,银行转账事务等都是分布式事务。
典型的场景就是微服务架构 微服务之间通过远程调用完成事务操作。比如:订单微服务和库存微服务,下单的同时订单微服务请求库存微服务减库存。简言之:跨JVM进程产生分布式事务。
什么是CAP原则?
CAP原则又叫CAP定理,同时又被称作布鲁尔定理(Brewer's theorem),指的是在一个分布式系统中,不可能同时满足以下三点。
一致性(Consistency)
指强一致性,在写操作完成后开始的任何读操作都必须返回该值,或者后续写操作的结果。
也就是说,在一致性系统中,一旦客户端将值写入任何一台服务器并获得响应,那么之后client从其他任何服务器读取的都是刚写入的数据
一致性保证了不管向哪台服务器写入数据,其他的服务器能实时同步数据
可用性(Availability)
可用性(高可用)是指:每次向未崩溃的节点发送请求,总能保证收到响应数据(允许不是最新数据)
分区容忍性(Partition tolerance)
分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务,也就是说,服务器A和B发送给对方的任何消息都是可以放弃的,也就是说A和B可能因为各种意外情况,导致无法成功进行同步,分布式系统要能容忍这种情况。除非整个网络环境都发生了故障。
为什么只能在A和C之间做出取舍?
分布式系统中,必须满足 CAP 中的 P,此时只能在 C/A 之间作出取舍。
如果选择了CA,舍弃了P,说白了就是一个单体架构。
一致性有几种分类?
CAP理论告诉我们只能在C、A之间选择,在分布式事务的最终解决方案中一般选择牺牲一致性来获取可用性和分区容错性。
这里的 “牺牲一致性” 并不是完全放弃数据的一致性,而是放弃强一致性而换取弱一致性。
一致性可以分为以下三种:
-
强一致性
-
弱一致性
-
最终一致性
强一致性
系统中的某个数据被成功更新后,后续任何对该数据的读取操作都将得到更新后的值。
也称为:原子一致性(Atomic Consistency)、线性一致性(Linearizable Consistency)
简言之,在任意时刻,所有节点中的数据是一样的。例如,对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性。
总结:
-
一个集群需要对外部提供强一致性,所以只要集群内部某一台服务器的数据发生了改变,那么就需要等待集群内其他服务器的数据同步完成后,才能正常的对外提供服务。
-
保证了强一致性,务必会损耗可用性。
弱一致性
系统中的某个数据被更新后,后续对该数据的读取操作可能得到更新后的值,也可能是更改前的值。
但即使过了不一致时间窗口这段时间后,后续对该数据的读取也不一定是最新值。
所以说,可以理解为数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性。
例如12306买火车票,虽然最后看到还剩下几张余票,但是只要选择购买就会提示没票了,这就是弱一致性。
最终一致性
是弱一致性的特殊形式,存储系统保证在没有新的更新的条件下,最终所有的访问都是最后更新的值。
不保证在任意时刻任意节点上的同一份数据都是相同的,但是随着时间的迁移,不同节点上的同一份数据总是在向趋同的方向变化。
简单说,就是在一段时间后,节点间的数据会最终达到一致状态。
总结
弱一致性即使过了不一致时间窗口,后续的读取也不一定能保证一致,而最终一致过了不一致窗口后,后续的读取一定一致。
什么是Base理论?
BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。
BA(Basic Available)基本可用
整个系统在某些不可抗力的情况下,仍然能够保证“可用性”,即一定时间内仍然能够返回一个明确的结果。这里是属于基本可用。
基本可用和高可用的区别:
-
“一定时间”可以适当延长 当举行大促(比如秒杀)时,响应时间可以适当延长
-
给部分用户返回一个降级页面 给部分用户直接返回一个降级页面,从而缓解服务器压力。但要注意,返回降级页面仍然是返回明确结果。
S(Soft State)柔性状态
称为柔性状态,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统不同节点的数据副本之间进行数据同步的过程存在延时。
E(Eventual Consisstency)最终一致性
同一数据的不同副本的状态,可以不需要实时一致,但一定要保证经过一定时间后仍然是一致的。
分布式事务有哪几种解决方案?
在分布式架构下,每个节点只知晓自己操作的失败或者成功,无法得知其他节点的状态。当一个事务跨多个节点时,为了保持事务的原子性与一致性,而引入一个协调者来统一掌控所有参与者的操作结果,并指示它们是否要把操作结果进行真正的提交或者回滚(rollback)。
2阶段提交(2PC)
二阶段提交协议(Two-phase Commit,即 2PC)是常用的分布式事务解决方案,即将事务的提交过程分为两个阶段来进行处理。
两个阶段分别为:
-
准备阶段
-
提交阶段
参与的角色:
-
事务协调者(事务管理器):事务的发起者
-
事务参与者(资源管理器):事务的执行者
准备阶段(投票阶段)
这是两阶段的第一段,这一阶段只是准备阶段,由事务的协调者发起询问参与者是否可以提交事务,但是这一阶段并未提交事务,流程图如下图:
准备提交阶段
-
协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待答复
-
各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)
-
如参与者执行成功,给协调者反馈同意,否则反馈中止
提交阶段
这一段阶段属于2PC的第二阶段(提交 执行阶段),协调者发起正式提交事务的请求,当所有参与者都回复同意时,则意味着完成事务,流程图如下:
提交事务阶段
-
协调者节点向所有参与者节点发出正式提交(
commit
)的请求。 -
参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
-
参与者节点向协调者节点发送ack完成消息。
-
协调者节点收到所有参与者节点反馈的ack完成消息后,完成事务。
但是如果任意一个参与者节点在第一阶段返回的消息为终止,或者协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时,那么这个事务将会被回滚,回滚的流程图如下:
回滚
-
协调者节点向所有参与者节点发出回滚操作(
rollback
)的请求。 -
参与者节点利用阶段1写入的undo信息执行回滚,并释放在整个事务期间内占用的资源。
-
参与者节点向协调者节点发送ack回滚完成消息。
-
协调者节点受到所有参与者节点反馈的ack回滚完成消息后,取消事务。
不管最后结果如何,第二阶段都会结束当前事务。
二阶段提交的事务正常提交的完整流程如下图:
事务正常提交完整流程
二阶段提交事务回滚的完整流程如下图:
事务回滚完整流程
举个百米赛跑的例子来具体描述下2PC的流程:学校运动会,有三个同学,分别是A,B,C,2PC流程如下:
-
裁判:A同学准备好了吗?准备进入第一赛道....
-
裁判:B同学准备好了吗?准备进入第一赛道....
-
裁判:C同学准备好了吗?准备进入第一赛道....
-
如果有任意一个同学没准备好,则裁判下达回滚指令
-
如果裁判收到了所有同学的OK回复,则再次下令跑......
-
裁判:1,2,3 跑............
-
A同学冲刺到终点,汇报给裁判
-
B,C同学冲刺失败,汇报给裁判
2PC的缺点
二阶段提交看起来确实能够提供原子性的操作,但是不幸的是,二阶段提交还是有几个缺点的:
-
性能问题:执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
-
可靠性问题:参与者发生故障。协调者需要给每个参与者额外指定超时机制,超时后整个事务失败。协调者发生故障。参与者会一直阻塞下去。需要额外的备机进行容错。
-
数据一致性问题:二阶段无法解决的问题:协调者在发出
commit
消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。 -
实现复杂:牺牲了可用性,对性能影响较大,不适合高并发高性能场景。
2PC的优点
-
尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证强一致)
3阶段提交(3PC)
三阶段提交协议,是二阶段提交协议的改进版本,三阶段提交有两个改动点。
-
在协调者和参与者中都引入超时机制
-
在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。
也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit
、PreCommit
、DoCommit
三个阶段。处理流程如下:
3PC时序图
阶段一:CanCommit阶段
3PC的CanCommit
阶段其实和2PC的准备阶段很像。协调者向参与者发送commit
请求,参与者如果可以提交就返回Yes响应,否则返回No响应。
-
事务询问:协调者向所有参与者发出包含事务内容的
canCommit
请求,询问是否可以提交事务,并等待所有参与者答复。 -
响应反馈:参与者收到
canCommit
请求后,如果认为可以执行事务操作,则反馈 yes 并进入预备状态,否则反馈 no。
CanCommit阶段流程如下图:
CanCommit阶段
阶段二:PreCommit阶段
协调者根据参与者的反应情况来决定是否可以进行事务的PreCommit
操作。根据响应情况,有以下两种可能。
-
假如所有参与者均反馈 yes,协调者预执行事务。
-
发送预提交请求 :协调者向参与者发送
PreCommit
请求,并进入准备阶段 -
事务预提交 :参与者接收到
PreCommit
请求后,会执行事务操作,并将undo
和redo
信息记录到事务日志中(但不提交事务) -
响应反馈 :如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。
-
PreCommit
-
假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。
-
发送中断请求 :协调者向所有参与者发送
abort
请求。 -
中断事务 :参与者收到来自协调者的
abort
请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。
-
PreCommit
阶段三:doCommit阶段
该阶段进行真正的事务提交,也可以分为以下两种情况。
进入阶段 3 后,无论协调者出现问题,或者协调者与参与者网络出现问题,都会导致参与者无法接收到协调者发出的 do Commit 请求或 abort 请求。此时,参与者都会在等待超时之后,继续执行事务提交。
-
执行提交
-
发送提交请求 协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送
doCommit
请求。 -
事务提交 参与者接收到
doCommit
请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。 -
响应反馈 事务提交完之后,向协调者发送ack响应。
-
完成事务 协调者接收到所有参与者的ack响应之后,完成事务。
-
docommit-提交事务
-
中断事务:任何一个参与者反馈 no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务
-
发送中断请求 如果协调者处于工作状态,向所有参与者发出 abort 请求
-
事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
-
反馈结果 参与者完成事务回滚之后,向协调者反馈ACK消息
-
中断事务 协调者接收到参与者反馈的ACK消息之后,执行事务的中断。
-
docommit-中断事务
优点
相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题,阶段 3 中协调者出现问题时,参与者会继续提交事务。
缺点
数据不一致问题依然存在,当在参与者收到 preCommit
请求后等待 doCommit
指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。
TCC(事务补偿)
TCC(Try Confirm Cancel)方案是一种应用层面侵入业务的两阶段提交。是目前最火的一种柔性事务方案,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。
TCC分为两个阶段,分别如下:
-
第一阶段:Try(尝试),主要是对业务系统做检测及资源预留 (加锁,锁住资源)
-
第二阶段:本阶段根据第一阶段的结果,决定是执行confirm还是cancel
-
Confirm(确认):执行真正的业务(执行业务,释放锁)
-
Cancle(取消):是预留资源的取消(出问题,释放锁)
-
TCC
为了方便理解,下面以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建 2 个步骤,库存服务和订单服务分别在不同的服务器节点上。
假设商品库存为 100,购买数量为 2,这里检查和更新库存的同时,冻结用户购买数量的库存,同时创建订单,订单状态为待确认。
①Try 阶段
TCC 机制中的 Try 仅是一个初步操作,它和后续的确认一起才能真正构成一个完整的业务逻辑,这个阶段主要完成:
-
完成所有业务检查( 一致性 ) 。
-
预留必须业务资源( 准隔离性 ) 。
-
Try 尝试执行业务。
Try阶段
②Confirm / Cancel 阶段
根据 Try 阶段服务是否全部正常执行,继续执行确认操作(Confirm)或取消操作(Cancel)。
Confirm 和 Cancel 操作满足幂等性,如果 Confirm 或 Cancel 操作执行失败,将会不断重试直到执行完成。
Confirm:当 Try 阶段服务全部正常执行, 执行确认业务逻辑操作,业务如下图:
Try->Confirm
这里使用的资源一定是 Try 阶段预留的业务资源。在 TCC 事务机制中认为,如果在 Try 阶段能正常的预留资源,那 Confirm 一定能完整正确的提交。
Confirm 阶段也可以看成是对 Try 阶段的一个补充,Try+Confirm 一起组成了一个完整的业务逻辑。
Cancel:当 Try 阶段存在服务执行失败, 进入 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 事务机制相比于上面介绍的 XA 事务机制,有以下优点:
-
性能提升:具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。
-
数据最终一致性:基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据的一致性。
-
可靠性:解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群。
缺点:
-
TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本。
本地消息表
本地消息表的方案最初是由 eBay 提出,核心思路是将分布式事务拆分成本地事务进行处理。
角色:
-
事务主动方
-
事务被动方
通过在事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。
这样可以避免以下两种情况导致的数据不一致性:
-
业务处理成功、事务消息发送失败
-
业务处理失败、事务消息发送成功
整体的流程如下图:
本地消息表
上图中整体的处理步骤如下:
-
①:事务主动方在同一个本地事务中处理业务和写消息表操作
-
②:事务主动方通过消息中间件,通知事务被动方处理事务通知事务待消息。消息中间件可以基于 Kafka、RocketMQ 消息队列,事务主动方主动写消息到消息队列,事务消费方消费并处理消息队列中的消息。
-
③:事务被动方通过消息中间件,通知事务主动方事务已处理的消息。
-
④:事务主动方接收中间件的消息,更新消息表的状态为已处理。
一些必要的容错处理如下:
-
当①处理出错,由于还在事务主动方的本地事务中,直接回滚即可
-
当②、③处理出错,由于事务主动方本地保存了消息,只需要轮询消息重新通过消息中间件发送,事务被动方重新读取消息处理业务即可。
-
如果是业务上处理失败,事务被动方可以发消息给事务主动方回滚事务
-
如果事务被动方已经消费了消息,事务主动方需要回滚事务的话,需要发消息通知事务主动方进行回滚事务。
优点
-
从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对 MQ 中间件特性的依赖。
-
方案轻量,容易实现。
缺点
-
与具体的业务场景绑定,耦合性强,不可公用。
-
消息数据与业务数据同库,占用业务系统资源。
-
业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限。
MQ事务方案(可靠消息事务)
基于 MQ 的分布式事务方案其实是对本地消息表的封装,将本地消息表基于 MQ 内部,其他方面的协议基本与本地消息表一致。
MQ事务方案整体流程和本地消息表的流程很相似,如下图:
MQ事务方案
从上图可以看出和本地消息表方案唯一不同就是将本地消息表存在了MQ内部,而不是业务数据库中。
那么MQ内部的处理尤为重要,下面主要基于 RocketMQ 4.3 之后的版本介绍 MQ 的分布式事务方案。
在本地消息表方案中,保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务,RocketMQ 的事务消息相对于普通 MQ提供了 2PC 的提交接口,方案如下:
正常情况:事务主动方发消息
事务主动方发消息
这种情况下,事务主动方服务正常,没有发生故障,发消息流程如下:
-
步骤①:发送方向 MQ 服务端(MQ Server)发送 half 消息。
-
步骤②:MQ Server 将消息持久化成功之后,向发送方 ack 确认消息已经发送成功。
-
步骤③:发送方开始执行本地事务逻辑。
-
步骤④:发送方根据本地事务执行结果向 MQ Server 提交二次确认(commit 或是 rollback)。
-
步骤⑤:MQ Server 收到 commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 rollback 状态则删除半消息,订阅方将不会接受该消息。
异常情况:事务主动方消息恢复
事务主动方消息恢复
在断网或者应用重启等异常情况下,图中 4 提交的二次确认超时未到达 MQ Server,此时处理逻辑如下:
-
步骤⑤:MQ Server 对该消息发起消息回查。
-
步骤⑥:发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
-
步骤⑦:发送方根据检查得到的本地事务的最终状态再次提交二次确认。
-
步骤⑧:MQ Server基于 commit/rollback 对消息进行投递或者删除。
优点
相比本地消息表方案,MQ 事务方案优点是:
-
消息数据独立存储 ,降低业务系统与消息系统之间的耦合。
-
吞吐量大于使用本地消息表方案。
缺点
-
一次消息发送需要两次网络请求(half 消息 + commit/rollback 消息) 。
-
业务处理服务需要实现消息状态回查接口。
最大努力通知
最大努力通知也称为定期校对,是对MQ事务方案的进一步优化。它在事务主动方增加了消息校对的接口,如果事务被动方没有接收到消息,此时可以调用事务主动方提供的消息校对的接口主动获取。
最大努力通知的整体流程如下图:
最大努力通知
在可靠消息事务中,事务主动方需要将消息发送出去,并且消息接收方成功接收,这种可靠性发送是由事务主动方保证的;
但是最大努力通知,事务主动方尽最大努力(重试,轮询....)将事务发送给事务接收方,但是仍然存在消息接收不到,此时需要事务被动方主动调用事务主动方的消息校对接口查询业务消息并消费,这种通知的可靠性是由事务被动方保证的。
最大努力通知适用于业务通知类型,例如微信交易的结果,就是通过最大努力通知方式通知各个商户,既有回调通知,也有交易查询接口。
Saga 事务
Saga 事务源于 1987 年普林斯顿大学的 Hecto 和 Kenneth 发表的如何处理 long lived transaction(长活事务)论文。
Saga 事务核心思想是将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。
Saga 事务基本协议如下:
-
每个 Saga 事务由一系列幂等的有序子事务(sub-transaction) Ti 组成。
-
每个 Ti 都有对应的幂等补偿动作 Ci,补偿动作用于撤销 Ti 造成的结果。
TCC事务补偿机制有一个预留(Try)动作,相当于先报存一个草稿,然后才提交;Saga事务没有预留动作,直接提交。
对于事务异常,Saga提供了两种恢复策略,分别如下:
向后恢复(backward recovery)
在执行事务失败时,补偿所有已完成的事务,是“一退到底”的方式。如下图:
向后恢复
从上图可知事务执行到了支付事务T3,但是失败了,因此事务回滚需要从C3,C2,C1依次进行回滚补偿。
对应的执行顺序为:T1,T2,T3,C3,C2,C1
这种做法的效果是撤销掉之前所有成功的子事务,使得整个 Saga 的执行结果撤销。
向前恢复(forward recovery)
也称之为:勇往直前,对于执行不通过的事务,会尝试重试事务,这里有一个假设就是每个子事务最终都会成功。
流程如下图:
向前恢复
适用于必须要成功的场景,事务失败了重试,不需要补偿。
Saga事务有两种不同的实现方式,分别如下:
-
命令协调(Order Orchestrator)
-
事件编排(Event Choreographyo)
命令协调
*协调器(Orchestrator,简称 OSO)以命令/回复的方式与每项服务进行通信,全权负责告诉每个参与者该做什么以及什么时候该做什么。整体流程如下图:
命令协调
上图步骤如下:
-
事务发起方的主业务逻辑请求 OSO 服务开启订单事务
-
OSO 向库存服务请求扣减库存,库存服务回复处理结果。
-
OSO 向订单服务请求创建订单,订单服务回复创建结果。
-
OSO 向支付服务请求支付,支付服务回复处理结果。
-
主业务逻辑接收并处理 OSO 事务处理结果回复。
*协调器必须事先知道执行整个订单事务所需的流程(例如通过读取配置)。如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚。
基于*协调器协调一切时,回滚要容易得多,因为协调器默认是执行正向流程,回滚时只要执行反向流程即可。
事件编排
没有*协调器(没有单点风险)时,每个服务产生并观察其他服务的事件,并决定是否应采取行动。
在事件编排方法中,第一个服务执行一个事务,然后发布一个事件。该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件。
当最后一个服务执行本地事务并且不发布任何事件时,意味着分布式事务结束,或者它发布的事件没有被任何 Saga 参与者听到都意味着事务结束。
事件编排
上图步骤如下:
-
事务发起方的主业务逻辑发布开始订单事件。
-
库存服务监听开始订单事件,扣减库存,并发布库存已扣减事件。
-
订单服务监听库存已扣减事件,创建订单,并发布订单已创建事件。
-
支付服务监听订单已创建事件,进行支付,并发布订单已支付事件。
-
主业务逻辑监听订单已支付事件并处理。
事件/编排是实现 Saga 模式的自然方式,它很简单,容易理解,不需要太多的代码来构建。如果事务涉及 2 至 4 个步骤,则可能是非常合适的。
优点
命令协调设计的优点如下:
-
服务之间关系简单,避免服务之间的循环依赖关系,因为 Saga 协调器会调用 Saga 参与者,但参与者不会调用协调器。
-
程序开发简单,只需要执行命令/回复(其实回复消息也是一种事件消息),降低参与者的复杂性。
-
易维护扩展,在添加新步骤时,事务复杂性保持线性,回滚更容易管理,更容易实施和测试。
事件/编排设计优点如下:
-
避免*协调器单点故障风险。
-
当涉及的步骤较少服务开发简单,容易实现。
缺点
命令协调设计缺点如下:
-
*协调器容易处理逻辑容易过于复杂,导致难以维护。
-
存在协调器单点故障风险。
事件/编排设计缺点如下:
-
服务之间存在循环依赖的风险。
-
当涉及的步骤较多,服务间关系混乱,难以追踪调测。
由于 Saga 模型中没有 Prepare 阶段,因此事务间不能保证隔离性。
当多个 Saga 事务操作同一资源时,就会产生更新丢失、脏数据读取等问题,这时需要在业务层控制并发,例如:在应用层面加锁,或者应用层面预先冻结资源。
总结
总结一下各个方案的常见的使用场景:
-
2PC/3PC:依赖于数据库,能够很好的提供强一致性和强事务性,但相对来说延迟比较高,比较适合传统的单体应用,在同一个方法中存在跨库操作的情况,不适合高并发和高性能要求的场景。
-
TCC:适用于执行时间确定且较短,实时性要求高,对数据一致性要求高,比如互联网金融企业最核心的三个服务:交易、支付、账务。
-
本地消息表/MQ 事务:都适用于事务中参与方支持操作幂等,对一致性要求不高,业务上能容忍数据不一致到一个人工检查周期,事务涉及的参与方、参与环节较少,业务上有对账/校验系统兜底。
-
Saga 事务:由于 Saga 事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。Saga 相比缺少预提交动作,导致补偿动作的实现比较麻烦,例如业务是发送短信,补偿动作则得再发送一次短信说明撤销,用户体验比较差。Saga 事务较适用于补偿动作容易处理的场景。
什么是Seata?
上面讲了这么多的分布式事务的理论知识,都没看到一个落地的实现,这不是吹牛逼吗?
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
-
对业务无侵入:即减少技术架构上的微服务化所带来的分布式事务问题对业务的侵入
-
高性能:减少分布式事务解决方案所带来的性能消耗
官方文档:https://seata.io/zh-cn/index.html
seata的几种术语:
-
TC(Transaction Coordinator):事务协调者。管理全局的分支事务的状态,用于全局性事务的提交和回滚。
-
TM(Transaction Manager):事务管理者。用于开启、提交或回滚事务。
-
RM(Resource Manager):资源管理器。用于分支事务上的资源管理,向 TC 注册分支事务,上报分支事务的状态,接收 TC 的命令来提交或者回滚分支事务。
AT模式
seata目前支持多种事务模式,分别有AT、TCC、SAGA 和 XA ,文章篇幅有限,今天只讲常用的AT模式。
AT模式的特点就是对业务无入侵式,整体机制分二阶段提交(2PC)
-
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
-
二阶段:
-
提交异步化,非常快速地完成
-
回滚通过一阶段的回滚日志进行反向补偿。
-
在 AT 模式下,用户只需关注自己的业务SQL,用户的业务SQL 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。
一个典型的分布式事务过程:
-
TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;
-
XID 在微服务调用链路的上下文中传播;
-
RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;
-
TM 向 TC 发起针对 XID 的全局提交或回滚决议;
-
TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
搭建Seata TC协调者
seata的协调者其实就是阿里开源的一个服务,我们只需要下载并且启动它。
下载地址:http://seata.io/zh-cn/blog/download.html
陈某下载的版本是
1.3.0
,各位最好和我版本一致,这样不会出现莫名的BUG。
下载完成后,直接解压即可。但是此时还不能直接运行,还需要做一些配置。
创建TC所需要的表
TC运行需要将事务的信息保存在数据库,因此需要创建一些表,找到seata-1.3.0源码的script\server\db
这个目录,将会看到以下SQL文件:
陈某使用的是Mysql数据库,因此直接运行mysql.sql这个文件中的sql语句,创建的三张表如下图:
修改TC的注册中心
找到seata-server-1.3.0\seata\conf
这个目录,其中有一个registry.conf
文件,其中配置了TC的注册中心和配置中心。
默认的注册中心是file
形式,实际使用中肯定不能使用,需要改成Nacos形式,改动的地方如下图:
需要改动的地方如下:
-
type:改成nacos,表示使用nacos作为注册中心
-
application:服务的名称
-
serverAddr:nacos的地址
-
group:分组
-
namespace:命名空间
-
username:用户名
-
password:密码
最后这份文件都会放在项目源码的根目录下,源码下载方式见文末
修改TC的配置中心
TC的配置中心默认使用的也是file
形式,当然要是用nacos作为配置中心了。
直接修改registry.conf
文件,需要改动的地方如下图:
需要改动的地方如下:
-
type:改成nacos,表示使用nacos作为配置中心
-
serverAddr:nacos的地址
-
group:分组
-
namespace:命名空间
-
username:用户名
-
password:密码
上述配置修改好之后,在TC启动的时候将会自动读取nacos的配置。
那么问题来了:TC需要存储到Nacos中的配置都哪些,如何推送过去?
在seata-1.3.0\script\config-center
中有一个config.txt
文件,其中就是TC所需要的全部配置。
在seata-1.3.0\script\config-center\nacos
中有一个脚本nacos-config.sh
则是将config.txt中的全部配置自动推送到nacos中,运行下面命令(windows可以使用git bash运行):
# -h 主机,你可以使用localhost,-p 端口号 你可以使用8848,-t 命名空间ID,-u 用户名,-p 密码
$ sh nacos-config.sh -h 127.0.0.1 -p 8080 -g SEATA_GROUP -t 7a7581ef-433d-46f3-93f9-5fdc18239c65 -u nacos -w nacos
推送成功则可以在Nacos中查询到所有的配置,如下图:
修改TC的数据库连接信息
TC是需要使用数据库存储事务信息的,那么如何修改相关配置呢?
上一节的内容已经将所有的配置信息都推送到了Nacos中,TC启动时会从Nacos中读取,因此我们修改也需要在Nacos中修改。
需要修改的配置如下:
## 采用db的存储形式
store.mode=db
## druid数据源
store.db.datasource=druid
## mysql数据库
store.db.dbType=mysql
## mysql驱动
store.db.driverClassName=com.mysql.jdbc.Driver
## TC的数据库url
store.db.url=jdbc:mysql://127.0.0.1:3306/seata_server?useUnicode=true
## 用户名
store.db.user=root
## 密码
store.db.password=Nov2014
在nacos中搜索上述的配置,直接修改其中的值,比如修改store.mode
,如下图:
当然Seata还支持Redis作为TC的数据库,只需要改动以下配置即可:
store.mode=redis
store.redis.host=127.0.0.1
store.redis.port=6379
store.redis.password=123456
启动TC
按照上述步骤全部配置成功后,则可以启动TC,在seata-server-1.3.0\seata\bin
目录下直接点击seata-server.bat
(windows)运行。
启动成功后,在Nacos的服务列表中则可以看到TC已经注册进入,如下图:
至此,Seata的TC就启动完成了............
Seata客户端搭建(RM)
上述已经将Seata的服务端(TC)搭建完成了,下面就以电商系统为例介绍一下如何编码实现分布式事务。
用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持:
-
仓储服务:对给定的商品扣除仓储数量。
-
订单服务:根据采购需求创建订单。
-
帐户服务:从用户帐户中扣除余额。
需要了解的知识:Nacos和openFeign,有不清楚的可以看我的前两章教程,如下:
仓储服务搭建
陈某整个教程使用的都是同一个聚合项目,关于Spring Cloud版本有不清楚的可以看我第一篇文章的说明。
添加依赖
新建一个seata-storage9020
项目,新增依赖如下:
由于使用的springCloud Alibaba
依赖版本是2.2.1.RELEASE
,其中自带的seata版本是1.1.0
,但是我们Seata服务端使用的版本是1.3.0,因此需要排除原有的依赖,重新添加1.3.0的依赖。
注意:seata客户端的依赖版本必须要和服务端一致。
创建数据库
创建一个数据库seata-storage
,其中新建两个表:
-
storage
:库存的业务表,SQL如下:
CREATE TABLE `storage` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`num` bigint(11) NULL DEFAULT NULL COMMENT '数量',
`create_time` datetime(0) NULL DEFAULT NULL,
`price` bigint(10) NULL DEFAULT NULL COMMENT '单价,单位分',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;
INSERT INTO `storage` VALUES (1, '码猿技术专栏', 1000, '2021-10-15 22:32:40', 100);
-
undo_log:回滚日志表,这是Seata要求必须有的,每个业务库都应该创建一个,SQL如下:
CREATE TABLE `undo_log` (
`branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
`xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id',
`context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` longblob NOT NULL COMMENT 'rollback info',
`log_status` int(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` datetime(6) NOT NULL COMMENT 'create datetime',
`log_modified` datetime(6) NOT NULL COMMENT 'modify datetime',
UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Compact;
配置seata相关配置
对于Nacos、Mysql数据源等相关信息就省略了,项目源码中都有。主要讲一下seata如何配置,详细配置如下:
spring:
application:
## 指定服务名称,在nacos中的名字
name: seata-storage
## 客户端seata的相关配置
seata:
## 是否开启seata,默认true
enabled: true
application-id: ${spring.application.name}
## seata事务组的名称,一定要和config.tx(nacos)中配置的相同
tx-service-group: ${spring.application.name}-tx-group
## 配置中心的配置
config:
## 使用类型nacos
type: nacos
## nacos作为配置中心的相关配置,需要和server在同一个注册中心下
nacos:
## 命名空间,需要server端(registry和config)、nacos配置client端(registry和config)保持一致
namespace: 7a7581ef-433d-46f3-93f9-5fdc18239c65
## 地址
server-addr: localhost:8848
## 组, 需要server端(registry和config)、nacos配置client端(registry和config)保持一致
group: SEATA_GROUP
## 用户名和密码
username: nacos
password: nacos
registry:
type: nacos
nacos:
## 这里的名字一定要和seata服务端中的名称相同,默认是seata-server
application: seata-server
## 需要server端(registry和config)、nacos配置client端(registry和config)保持一致
group: SEATA_GROUP
namespace: 7a7581ef-433d-46f3-93f9-5fdc18239c65
username: nacos
password: nacos
server-addr: localhost:8848
以上配置注释已经很清楚,这里着重强调以下几点:
-
客户端seata中的nacos相关配置要和服务端相同,比如地址、命名空间..........
-
tx-service-group:这个属性一定要注意,这个一定要和服务端的配置一致,否则不生效;比如上述配置中的,就要在nacos中新增一个配置
service.vgroupMapping.seata-storage-tx-group=default
,如下图:
注意:
seata-storage-tx-group
仅仅是后缀,要记得添加配置的时候要加上前缀service.vgroupMapping.
扣减库存的接口
逻辑很简单,这里仅仅是做了减库存的操作,代码如下:
这里的接口并没有不同,还是使用@Transactional
开启了本地事务,并没有涉及到分布式事务。
到这里仓储服务搭建好了..............
账户服务搭建
搭建完了仓储服务,账户服务搭建很类似了。
添加依赖
新建一个seata-account9021
服务,这里的依赖和仓储服务的依赖相同,直接复制
创建数据库
创建一个seata-account
数据库,其中新建了两个表:
-
account
:账户业务表,SQL如下:
CREATE TABLE `account` (
`id` bigint(11) NOT NULL,
`user_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户userId',
`money` bigint(11) NULL DEFAULT NULL COMMENT '余额,单位分',
`create_time` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;
INSERT INTO `account` VALUES (1, 'abc123', 1000, '2021-10-19 17:49:53');
-
undo_log:回滚日志表,同仓储服务
配置seata相关配置
Seata相关配置和仓储服务相同,只不过需要在nacos中添加一个service.vgroupMapping.seata-account-tx-group=default
,如下图:
扣减余额的接口
具体逻辑自己完善,这里我直接扣减余额,代码如下:
依然没有涉及到分布式事务,还是使用@Transactional
开启了本地事务,是不是很爽............
订单服务搭建(TM)
这里为了节省篇幅,陈某直接使用订单服务作为TM,下单、减库存、扣款整个流程都在订单服务中实现。
添加依赖
新建一个seata-order9022
服务,这里需要添加的依赖如下:
-
Nacos服务发现的依赖
-
seata的依赖
-
openFeign的依赖,由于要调用账户、仓储的微服务,因此需要额外添加一个openFeign的依赖
创建数据库
新建一个seata_order
数据库,其中新建两个表,如下:
-
t_order
:订单的业务表
CREATE TABLE `t_order` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`product_id` bigint(11) NULL DEFAULT NULL COMMENT '商品Id',
`num` bigint(11) NULL DEFAULT NULL COMMENT '数量',
`user_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户唯一Id',
`create_time` datetime(0) NULL DEFAULT NULL,
`status` int(1) NULL DEFAULT NULL COMMENT '订单状态 1 未付款 2 已付款 3 已完成',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;
-
undo_log:回滚日志表,同仓储服务
配置和seata相关配置
Seata相关配置和仓储服务相同,只不过需要在nacos中添加一个service.vgroupMapping.seata-order-tx-group=default
,如下图:
扣减库存的接口
这里需要通过openFeign调用仓储服务的接口进行扣减库存,接口如下:
以上只是简单的通过openFeign调用,更细致的配置,比如降级,自己完善.........
扣减余额的接口
这里仍然是通过openFeign调用账户服务的接口进行扣减余额,接口如下:
创建订单的接口
下订单的接口就是一个事务发起方,作为TM,需要发起一个全局事务,详细代码如下图:
有什么不同?不同之处就是使用了@GlobalTransactional
而不是@Transactional
。
@GlobalTransactional
是Seata提供的,用于开启才能全局事务,只在TM中标注即可生效。
测试
分别启动seata-account9021
、seata-storage9020
、seata-order9022
,如下图:
下面调用下单接口,如下图:
从控制台输出的日志可以看出,流程未出现任何异常,事务已经提交,如下图:
果然,查看订单、余额、库存表,数据也都是正确的。
但是,这仅仅是流程没问题,并不能说明分布式事务已经配置成功了,因此需要手动造个异常。
在扣减余额的接口睡眠2秒钟,因为openFeign的超时时间默认是1秒,这样肯定是超时异常了,如下图:
此时,调用创建订单的接口,控制台日志输出如下图:
发现在扣减余额处理中超时了,导致了异常.......
此时,看下库存的数据有没有扣减,很高兴,库存没有扣减成功,说明事务已经回滚了,分布式事务成功了。
总结
Seata客户端创建很简单,需要注意以下几点内容:
-
seata客户端的版本需要和服务端保持一致
-
每个服务的数据库都要创建一个
undo_log
回滚日志表 -
客户端指定的事务分组名称要和Nacos相同,比如
service.vgroupMapping.seata-account-tx-group=default
-
前缀:
service.vgroupMapping.
-
后缀:
{自定义}
-
项目源码已经上传,关注公众号
码猿技术专栏
回复关键词9528
获取!
AT模式原理分析
AT模式最大的优点就是对业务代码无侵入,一切都像在写单体业务逻辑一样。
TC相关的三张表:
-
global_table
:全局事务表,每当有一个全局事务发起后,就会在该表中记录全局事务的ID -
branch_table
:分支事务表,记录每一个分支事务的ID,分支事务操作的哪个数据库等信息 -
lock_table
:全局锁
一阶段步骤
-
TM:seata-order.create()
方法执行时,由于该方法具有@GlobalTranscational
标志,该TM会向TC发起全局事务,生成XID(全局锁) -
RM:StorageService.deduct()
:写表,UNDO_LOG记录回滚日志(Branch ID),通知TC操作结果 -
RM:AccountService.deduct()
:写表,UNDO_LOG记录回滚日志(Branch ID),通知TC操作结果 -
RM:OrderService.create()
:写表,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 模式是一种对业务无任何侵入的分布式事务解决方案。
总结
本文介绍了七种分布式事务解决方案,以及阿里开源的Seata,从入门到实现。