本文章仅供个人学习使用。
参考
- Saga事务:www.cnblogs.com/sheng-jie/p…
- 2PC/3PC:zhuanlan.zhihu.com/p/183753774
- TCC:www.cnblogs.com/crazymakerc…
本文将介绍哪些分布式事务?
粗浅的读了一些文章,发现分布式事务实现方式很多,而且分类方式各不相同。我姑且以下面思维导图的方式分类吧。
1. 2PC
过程
过程文字描述:
- 协调者在 准备阶段 向所有参与者发送准备命令。
- 参与者收到准备指令后,执行除了提交之外的所有工作,之后给协调者返回响应。
- 协调者 阻塞等待 所有参与者响应了,之后进入提交阶段。如果所有参与者准备阶段全部响应成功,则向所有参与者发送提交事务的命令。只要有一个失败,则向所有参与者发送回滚事务的命令。
- 参与者收到 提交/回滚 命令后,执行对应的动作,并给协调者响应。
- 如果执行失败,只能不断重试(协调者一段时间没有收到某个参与者的 提交/回滚 成功的响应,就重新向其发送提交/回滚 命令)。
- 等协调者收到所有参与者的响应后,才向客户端返回 事务成功/失败。
流程图(图片来源:zhuanlan.zhihu.com/p/183753774 ) :
哪里体现了强一致性?
- 协调者在准备阶段后,会阻塞等待所有参与者返回结果。
- 协调者在提交阶段,会阻塞等待所有参与者返回结果,最后才给客户端返回结果。
优缺点
特点:牺牲了可用性A,来换取一致性C和分区容错性P。
优点:
- 强一致性:客户端收到结果时,数据库状态是一致的。
缺点:
- 单点故障:协调者在发送提交事务前挂了,导致所有参与者的资源都无法释放。
- 阻塞:协调者会阻塞等待。
应用
传统数据库的分布式事务(MySQL XA,Oracle)
2. 3PC
过程
1. CanCommit 阶段(询问阶段)
- 协调者:向所有参与者发送 CanCommit? 请求。
- 参与者:检查能否执行事务,如果没问题就返回 Yes,否则返回 No。
2. PreCommit 阶段(预提交阶段)
-
如果所有参与者都回复 Yes:
- 协调者发送 PreCommit 请求。
- 参与者执行事务操作,但不提交,写入日志,锁定资源,并返回 ACK。
-
如果有任意一个 No,协调者直接发送 Abort。
3. DoCommit 阶段(提交阶段)
-
如果协调者收到了所有参与者的 ACK:
- 发送 DoCommit,要求参与者正式提交。
-
如果有超时/失败:
- 协调者发送 Abort,要求回滚。
问题:数据不一致
尽管 3PC 改进了阻塞问题,但它 牺牲了一致性。
- 协调者进入 PreCommit 阶段,向参与者 A、B 发送 PreCommit。
- 假设 A 收到并成功执行预提交,返回 ACK;但是 B 没收到(网络分区)。
- 协调者以为 B 超时,决定发送 Abort。
- A 的视角:已经执行了预提交,认为只差最后一步,于是可能超时后自行提交。
- B 的视角:没收到 PreCommit,根本没执行,或者收到了 Abort,就回滚。
👉 最终结果:A 提交成功,B 回滚,出现数据不一致。
改进点
- 引入超时机制:参与者或协调者在等待过程中超时,可以自行决定回滚或提交,避免无限阻塞。
应用
实际工程中很少用
3. Saga事务
过程
Saga事务的核心思想是,把一个长事务拆分成多个子事务,每个子事务都有一个补偿事务,如果某个子事务执行失败,则执行所有之前的补偿事务来实现回滚。
比如,一个电商系统分为:订单服务、库存服务、支付服务。下订单后库存服务要锁库存,支付成功后,要扣减库存。那么下单的补偿事务就是取消订单、锁库存的补偿事务就是解锁库存、扣减库存的补偿事务就是增加库存。如果扣减库存这一步失败了,那么就执行之前所有的补偿事务,即返还库存、解锁库存、取消订单。
特点
- 异步:性能高,减少加锁时间。
所有的本地子事务执行过程中,都无需等待其调用的子事务执行,减少了加锁的时间,这在事务流程较多较长的业务中性能优势更为明显。
同时,其利用队列进行进行通讯,具有削峰填谷的作用。
因此该形式适用于不需要同步返回发起方执行最终结果、可以进行补偿、对性能要求较高、不介意额外编码的业务场景。
适用场景
- SAGA适用于无需马上返回业务发起方最终状态的场景,例如:你的请求已提交,请稍后查询或留意通知 之类。
- 事务流程较多较长。
4. TCC
最终一致性。
过程
分为协调者和参与者2种角色,参与者必须在业务层面实现3个接口:Try,Confirm,Cancel。
- 协调者启动事务,生成事务ID,在本地保存事务(status=created)。
- 协调者调用所有参与者的Try接口,并更新事务状态(status=try)。
- 如果所有参与者都返回成功,则进入Confirm阶段,否则进入Cancel阶段。
- 协调者调用所有参与者的Confirm接口,等待所有参与者返回成功,更新事务状态(status=confirmed)。如果调用某个参与者的confirm接口失败,则需要重试。重试需要注意通过事务ID来保持幂等。
- 协调者给客户端返回事务失败/成功。
参与者这边,就是需要通过保存事务ID来实现幂等。
TCC vs 2PC
看起来TCC跟2PC非常相似,实际上TCC也包含着2PC的思想,但是TCC工作在业务层,2PC工作在资源层。
TCC的问题:侵入性
它的一个问题在于,需要每个参与者都分别实现Try,Confirm和Cancel接口及逻辑,这对于业务的侵入性是巨大的。
TCC 方案严重依赖回滚和补偿代码,最终的结果是:回滚代码逻辑复杂,业务代码很难维护。
TCC的应用场景
支付,交易相关的场景会用TCC。
5. MQ事务消息方案
这个方案,以及 本地消息表 的方案,主要保证的是:本地事务和消息的一致性
过程
- 事务发起方首先发送半消息到MQ;
- MQ通知发送方消息发送成功;
- 在发送半消息成功后执行本地事务;
- 根据本地事务执行结果返回commit或者是rollback;
- 如果消息是rollback, MQ将丢弃该消息不投递;如果是commit,MQ将会消息发送给消息订阅方;
- 订阅方根据消息执行本地事务;
- 订阅方执行本地事务成功后再从MQ中将该消息标记为已消费;
- 如果执行本地事务过程中,执行端挂掉,或者超时,MQ服务器端将不停的询问producer来获取事务状态;
- Consumer端的消费成功机制由MQ保证;
用RocketMQ具体实现
1.producer(本例中指A系统)发送半消息到broker,这个半消息不是说消息内容不完整, 它包含完整的消息内容, 在producer端和普通消息的发送逻辑一致
2.broker存储半消息,半消息存储逻辑与普通消息一致,只是属性有所不同,topic是固定的RMQ_SYS_TRANS_HALF_TOPIC,queueId也是固定为0,这个tiopic中的消息对消费者是不可见的,所以里面的消息永远不会被消费。这就保证了在半消息提交成功之前,消费者是消费不到这个半消息的
3.broker端半消息存储成功并返回后,A系统执行本地事务,并根据本地事务的执行结果来决定半消息的提交状态为提交或者回滚
4.A系统发送结束半消息的请求,并带上提交状态(提交 or 回滚)
5.broker端收到请求后,首先从RMQ_SYS_TRANS_HALF_TOPIC的queue中查出该消息,设置为完成状态。如果消息状态为提交,则把半消息从RMQ_SYS_TRANS_HALF_TOPIC队列中复制到这个消息原始topic的queue中去(之后这条消息就能被正常消费了);如果消息状态为回滚,则什么也不做。
6.producer发送的半消息结束请求是 oneway 的,也就是发送后就不管了,只靠这个是无法保证半消息一定被提交的,rocketMq提供了一个兜底方案,这个方案叫消息反查机制,Broker启动时,会启动一个TransactionalMessageCheckService 任务,该任务会定时从半消息队列中读出所有超时未完成的半消息,针对每条未完成的消息,Broker会给对应的Producer发送一个消息反查请求,根据反查结果来决定这个半消息是需要提交还是回滚,或者后面继续来反查
7.consumer(本例中指B系统)消费消息,执行本地数据变更(至于B是否能消费成功,消费失败是否重试,这属于正常消息消费需要考虑的问题)
6. 本地消息表
过程
发送消息方:
- 需要有一个消息表,记录着消息状态相关信息。
- 业务数据和消息表在同一个数据库,要保证它俩在同一个本地事务。直接利用本地事务,将业务数据和事务消息直接写入数据库。
- 在本地事务中处理完业务数据和写消息表操作后,通过写消息到 MQ 消息队列。使用专门的投递工作线程进行事务消息投递到MQ,根据投递ACK去删除事务消息表记录
- 消息会发到消息消费方,如果发送失败,即进行重试。
消息消费方:
- 处理消息队列中的消息,完成自己的业务逻辑。
- 如果本地事务处理成功,则表明已经处理成功了。
- 如果本地事务处理失败,那么就会重试执行。
- 如果是业务层面的失败,给消息生产方发送一个业务补偿消息,通知进行回滚等操作。