分布式事务场景
事务核心特征
- 原子性
- 要么全部成功
- 要么全部失败
- 隔离性
- 并发事务互不干扰
- 持久性
- 事务提交后,变更永久有效
- 一致性
- 宏观视角下的全局数据保持一致
分布式事务场景问题
处理一笔创建订单的请求时,需要执行两步操作
- 从账户系统中,扣减用户的账户余额
- 从库存系统中,扣减商品的剩余库存
事务消息方案
基于MQ实现分布式事务
依赖于 MQ 中 at least once 的性质,可以简单认为,只要一条消息被投递到了 MQ,那么这条消息一定会被下游消费。
倘若一个事务流程包含在服务 A 中执行动作 I 以及在服务 B 中执行动作 II,那么可以依据一下步骤使用 MQ 进行串联:
- 服务 A 作为 MQ 的 producer,服务 B 作为 MQ 的 consumer
- 服务 A 执行动作 I,执行成功后往 MQ 中投递消息
- 服务 B 接收到消息后,完成动作 II
优势
- 将服务 A 和服务 B 解耦,提高系统的吞吐量
- 当动作 I 执行失败后,可以选择不发送消息,从而熔断流程
- 基于MQ 的 at least once 语义,服务 B 一定能够收到消息
- 依赖于 MQ 的 ack 机制,服务 B 可以进行有限次数的重试,提高动作 II 的执行成功率
缺点
- 由于无法对上游动作 I 进行回滚,那么动作 II 在客观上无法成功完成时,就无法保证原子性
- 服务 A 实际上执行的两步操作:执行动作、投递消息,不能保证原子性
下面将介绍如何保证执行动作(本地事务)、投递消息两步操作的原子性
原子性:本地事务和消息投递
方案一:将消息投递动作包含进本地事务中
缺点:
- 本地事务操作中参杂了对第三方组件的操作,可能导致长事务问题
- 本地事务和 MQ 强耦合,如果 MQ 成功发送了消息,但是获取响应时超时了,那么机制会误判,对本地事务进行回滚
- 如果 MQ 成功发送消息,但是事务提交失败:此时本地事务中的数据自然而然地回滚,MQ 中的消息却覆水难收
方案二:事务消息
使用 RocketMQ 中的事务消息,本质还是两阶段提交的过程(类似于 redolog 和 binlog 一致性的做法)
为了避免本地事务未及时给MQ 响应,MQ 会定时(轮训)向 producer 查询本地事务的执行结果
缺点
- 不具备逆向回滚的功能
- 硬伤
- 流程高度抽象:事务消息方案通过将分布式事务概括为本地事务 + 消息投递两个动作。实际上一个分布式事务中可能会涉及很多操作流程,笼统的概括为本地事务 + 消息投递,可能会导致大量繁重的操作被聚焦在本地事务中,会给 producer 带来极大的压力。
TCC
在 TCC 分布式事务框架中,主要有三类角色:
- 应用方 Application
- 指的是需要使用 TCC 分布式事务框架的应用方
- 事务协调器 TX Manager
- 负责统筹分布式事务的执行
- 实现TCC Component 的注册管理
- 负责和 Application 交互,提供分布式事务的创建入口,基于 Application 事务执行结果的响应
- 串联 Try -> Confirm / Cancel 的两阶段流程。在第一阶段中批量调用 Try 接口,在第二阶段中批量调用 Confirm/Cancel 接口
- TCC 组件 TCC Component
- 指的是在分布式事务中需要完成特定操作的子模块。这个模块通常负责一些状态数据的维护和更新操作,需要对外暴露出 Try、Confirm和 Cancel 三个接口
- Try:锁定资源,通常类似于冻结的语义,保留后续变化的可能性
- Confirm:对 Try 操作进行二次确认,将记录中的冻结状态修改为成功状态
- Cancel:对 Try 操作继续回滚,将记录中的冻结状态消除或修改为失败状态,将底层对应的数据状态进行回滚
优势:
- 任意一个 Component 的 Try 操作失败了,都能对整体进行回滚
- TX Manager 已经通过 Try 操作,让 Component 提前锁定了对应的资源,因此确保了资源是充足的,且由于提前执行了状态锁定,因此发生并发问题的概率更小
缺点:
- 实现成本很高
- 数据状态只能趋于最终一致性,无法做到即时一致性