分布式事务分两大类,一类是XA类型的,一类是基于消息通知的事务方案。前些日子写了分布式事务-2PC与TCC,这次聊一下Saga和基于消息的的事务方案。
Saga
SAGA最初出现在1987年Hector Garcaa-Molrna & Kenneth Salem发表的论文SAGAS里。其核心思想是将长事务拆分为多个本地短事务,由Saga事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。
流程
执行成功
如果我们要进行一个类似于银行跨行转账的业务,转出(TransOut)和转入(TransIn)分别在不同的微服务里,一个成功完成的SAGA事务典型的时序图如下:
执行失败
如果有正向操作失败,则需要调用各分支的补偿操作,进行回滚,最后事务成功回滚。所以对每个事务分支而言,要包括action和compensate两个操作。
补偿失败
按照Saga模式的协议,补偿操作是不允许失败的,遇见失败的情况,都是由于临时故障或者程序bug。补偿操作遇见失败时,会不断进行重试,直到成功。为了避免程序bug导致补偿操作一直无法成功,建议开发者对全局事务表进行监控,发现重试超过3次的事务,发出报警,由运维人员找开发手动处理。
设计要点
空补偿
同TCC模式,当遇到网络问题时,事务参与者可能会只收到补偿操作的请求,所以补偿操作要求能「空补偿」
防悬挂
同TCC模式,当遇到网络问题时,事务参与者可能会先于执行操作收到补偿操作的请求,所以执行操作要求能「防悬挂」
幂等性
执行操作和补偿操作的实现必须是幂等的。
状态机引擎
Saga模式通常会通过状态机引擎来实现,状态机引擎可以编排服务的调用流程和补偿服务的拓扑关系。这种状态机可以基于MQ的异步事件驱动,可以极大的提高吞吐量,其实基于事件通知的分布式事务可以看成是Saga模式的一种特例。
伪代码
我们以下单流程,需要使用优惠券和库存为例,写一下使用Saga生成订单的伪代码。
- 生成订单号
- 计算使用哪张优惠券、使用哪个库存,确定订单真正的金额等信息
- 向事务管理器注册使用优惠券、使用库存、生成订单的执行、补偿行为,设置先执行使用优惠券和库存,后执行生成订单
- 事务管理器记录主事务和6个分支事务(使用优惠券、使用库存、生成点单的执行、补偿行为)
- 事务管理器调用执行使用优惠券、使用库存、生成订单操作
- 如果正向全部成功,事务管理器返回成功,如果有失败,事务管理器进行补偿(补偿失败事务管理器会不断重试),并返回失败
- 主程序根据返回结果,显示订单创建成功与失败
Saga总结
因为正常情况下每个事务参与者都会直接提交,而不需要等待其他参与者的状态,所以Saga模式的并发性能非常好。但隔离性比较差,它的隔离级别是读未提交,这就意味着一个事务可以读到其他事务还没未提交的数据状态。
对于业务的接入成本,只需要为Saga单独实现一个补偿操作,而对于补偿操作的实现要求也不是很高,所以Saga的接入成本相对来说比较低。
综上,Saga模式比较适合于对并发性能和业务接入成本要求比较高,但是对于有隔离性要求不高的场景,如果有遗留系统要接入,考虑到Saga的接入成本比较低,所以这时也适合Saga模式。
基于消息的分布式事务
基于消息的分布式事务与上面的方案很不同,适合执行周期长且实时性要求不高的场景。基于消息的分布式事务是指当事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)一定能够接收消息并处理事务成功,此方案强调的是只要消息发给事务参与方最终事务要达到一致。一般使用消息中间件完成。
事务发起方(消息生产方)将消息发给消息中间件,事务参与方从消息中间件接收消息,事务发起方和消息中间件之间,事务参与方(消息消费方)和消息中间件之间都是通过网络通信。
问题
要使用消息实现分布式事务,需要解决三个问题:
- 本地事务与消息发送的原子性问题
- 保证数据库操作与发送消息的一致性,不会出现只有一个成功,另一个不成功的情况
- 事务参与方接收消息的可靠性
- 事务参与方必须能够从消息队列接收到消息,如果接收消息失败可以重复接收消息
- 消息重复消费的问题
- 由于网络的存在,若某一个消费节点超时但是消费成功,此时消息中间件会重复投递此消息,就导致了消息的重复消费
流程
使用RocketMQ事务消息,能够解决以上问题。
Apache RocketMQ 4.3之后的版本正式支持事务消息,为分布式事务实现提供了便利性支持。RocketMQ 事务消息设计则主要是为了解决 Producer 端的消息发送与本地事务执行的原子性问题,RocketMQ 的设计中 broker 与 producer 端的双向通信能力,使得 broker 天生可以作为一个事务协调者存在;而 RocketMQ 本身提供的存储机制为事务消息提供了持久化能力;RocketMQ 的高可用机制以及可靠消息设计则为事务消息在系统发生异常时依然能够保证达成事务的最终一致性。
- Producer 发送事务消息
本例中,Producer 即MQ发送方。
Producer (MQ发送方)发送事务消息至MQ Server,MQ Server将消息状态标记为Prepared(预备状态),注意此时这条消息消费者(MQ订阅方)是无法消费到的。
- MQ Server回应消息发送成功
MQ Server接收到Producer 发送给的消息则回应发送成功表示MQ已接收到消息。
- Producer 执行本地事务
Producer 端执行业务代码逻辑,通过本地数据库事务控制。
- 消息投递
若Producer 本地事务执行成功则自动向MQServer发送commit消息,MQ Server接收到commit消息后将”状态标记为可消费,此时MQ订阅方即正常消费消息;
若Producer 本地事务执行失败则自动向MQServer发送rollback消息,MQ Server接收到rollback消息后将删除消息 。
MQ订阅方消费消息,消费成功则向MQ回应ack,否则将重复接收消息。这里ack默认自动回应,即程序执行正常则自动回应ack。
- 事务回查
如果执行Producer端本地事务过程中,执行端挂掉,或者超时,MQ Server将会不停的询问同组的其他 Producer来获取事务执行状态,这个过程叫事务回查。MQ Server会根据事务回查结果来决定是否投递消息。
以上主干流程已由RocketMQ实现,对用户侧来说,用户需要分别实现本地事务执行以及本地事务回查方法,因此只需关注本地事务的执行状态即可。
基于消息的分布式事务总结
基于消息的分布式事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消息执行的异步操作, 避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦。
总结
分布式事务的几种主要类型都讲完了,每种类型有不同的特性,大家根据需要选择。但讲的这些都是理论,大家有时间可实现个Demo或者看一下源码,毕竟动手才能真正掌握知识。
如果是Go语言,可以看一下DTM,github地址为https://github.com/yedf/dtm ,执行go run app/main.go qs便能查看效果。
另外使用事务管理器后,性能会受TM影响,可考虑单元化方案进行解决。
资料
- TCC Demo 代码实现
- Hmily实现TCC事务
- https://github.com/yedf/dtm
- https://dtm.pub/
- RocketMQ实现可靠消息最终一致性
最后
大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)
我的个人博客为:https://shidawuhen.github.io/