分布式事务
Table of Contents
1 分布式事务
分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于 不同的分布式系统的不同节点之上
- 事务参与者 (APP)
- 前端处理请求的应用
- 事务管理器 (TM)
- 后台协调事务的应用
- 资源服务器 (RM)
- 一般指的是数据库中的数据
2 XA 协议
XA 协议是一个基于数据库的分布式事务协议,其分为两部分
- 事务管理器和本地资源管理器
- 事务管理器作为一个全局的调度者, 负责对各个本地资源管理器统一号令提交或者回滚
- 如今 Oracle, Mysql 等数据库均已实现了 XA 接口
3 2PC & 3PC
3.1 提交模型
2PC 和 3PC 参与事务提交的处理流程如下,事务包含如下三类角色
- 单业务发起方 APP
- 单事务管理器 TM
- 多资源管理器 RM
[APP] -> [ TM ] -> [RM_1] | | | +----> [RM_2] | +-------> [RM_3]
3.2 2PC (two-phase-commit) 两阶段事务
2PC 的具体的实现流程如下
- Prepare 阶段
- APP 向 TM 发送请求,请求执行一个操作
- TM 向所有参与的 RM 发送 预请求, RM 需要报告能否执行操作
- TM 可以向 MQ 消息队列发送个半消息 (Half Message)
- RM 打开本地事务,执行到待提交的阶段
- 然后反馈能否成功执行事务
- Commit 阶段
- TM 接收所有 RM 的报告
- TM 接收到所有的报告,如果所有 RM 表示可以成功执行
- TM 发送全局提交 (Global Commit) 信号
- 所有 RM 提交事务
- TM 接收到所有的报告,如果有一个 RM 表示不能成功执行
- TM 发送全局回滚 (Global Commit) 信号
- 所有 RM 回滚事务
2PC 存在问题
- RM 的事务一直挂起可能存在 性能问题
- TM 是单点的,可能存在 单点故障
- Commit 阶段出现消息丢失会导致数据不一致
3.3 3PC (three-phase-commit) 三阶段事务
3PC 在 2PC 的基础上增加了 CanCommit 阶段
- 增加了 超时机制, 可以预先判断任何 RM 是否宕机,减少超时交互的成本
- 有效地解决了 RM 的 单点故障 问题
3PC 的具体的实现流程如下
- CanCommit 阶段
- APP 向 TM 发送请求,请求执行一个操作
- TM 向所有参与的 RM 发送 自检操作, 所有 RM 完成自检回复 Yes/No
- 注意,这里 RM 并没有开启本地事务
- 如果 TM 收到任何一个 RM 回复 No,事务操作终止
- 如果任何一个 TM 超时,事务操作终止
- PreCommit 阶段
- 在 CanCommit 阶段中 TM 都回复 Yes
- 接着进行 PreCommit 阶段,该阶段类似 2PC 的 Prepare 阶段
- TM 向所有参与的 RM 发送 预请求, RM 需要报告能否执行操作
- 所有 RM 执行本地事务,记录 Undo/Redo 日志
- 如果 RM 确认可以执行,向 TM 返回 ACK 确认
- doCommit 阶段
- 该阶段类似于 2PC 的 Commit 阶段
- TM 接收所有 RM 的报告
- TM 接收到所有的报告,如果所有 RM 表示可以成功执行
- TM 发送全局提交 (Global Commit) 信号
- 所有 RM 提交事务
- TM 接收到所有的报告,如果有一个 RM 表示不能成功执行
- TM 发送全局回滚 (Global Commit) 信号
- 所有 RM 回滚事务
4 TCC (Try-Confirm-Cancel) 补偿事务
4.1 提交模型
TCC 参与事务提交的处理流程如下,事务包含如下三类角色
- 单业务发起方 APP
- 单事务管理器 TM
- 多资源管理器 RM
- 每个 RM 需要实现 Try, Confirm, Cancel 接口
[APP] ->[ TM ]-> [RM_1] | | | +---> [RM_2] | +------> [RM_3]
4.2 TCC 三个操作说明
- Try 操作
- 完成业务检查,预留业务所需的资源
- Confirm 操作
- 执行业务逻辑
- Cancel 操作
- 释放 Try 操作预留的业务资源
4.3 TCC 实践案例
- 汇款服务
- Try: 处理预扣款逻辑
- 检查 A 账户的有效性
- 检查 A 账户的余额是否充足
- 从 A 账户中扣减 100 元,并将状态置为 "转账中"
- 预留扣减资源,将 "从 A 账户向 B 账户转账 100 元" 这个事件存入消息或日志
- Confirm: 不做任何操作
- Cancel: A 账户增加 100 元;从日志或消息中释放扣减资源
- Try: 处理预扣款逻辑
- 收款服务
- Try: 检查 B 账户的有效性
- Confirm: 读取日志或者消息, B 账户增加 100 元;从日志或消息中释放扣减资源
- Cancel: 不做任何操作
由此可以看出,TCC 模型对业务的侵入性较强,改造的难度较大
4.4 TCC 缺点
- 应用侵入性强
- TCC 由于基于在业务层面, 开发时需要在业务层面保证事务的有效性
- 至使每个操作都需要有 Try, Confirm, Cancel 三个接口
- 开发难度大
- 代码开发量很大
- Confirm 和 Cancel 接口还必须实现幂等性才能保证数据一致性
5 本地消息表方法
PK | Status |
---|---|
1001 | 0 |
1002 | 1 |
1005 | 0 |
1007 | 1 |
- 就是上游服务先执行操作, 将操作记录到数据库中的本地消息表, 并且此时这条操作 记录的 Status 设置为 0, 也就是未通知成功
- 然后将操作记录封装成 Kafka 消息发送到消息队列, 下游系统接受到, 进行消费, 然 后消费成功后调用上游服务的接口, 通知他消费成功了, 上游系统将本地消息表中这 条记录的 Status 设置为 1, 代表通知成功
- 并且上游系统会定时扫描本地消息表, 将 Status 为 0 的操作记录, 封装成 Kafka 消息, 发送到消息队列
- 并且下游系统是通过消息中操作记录的主键 ID 来防止不重复消费, 保证幂等性的。 就是消费消息时, 发送操作记录的 ID 已经在数据库中存在了, 就代表之前已经处理 过了, 不处理这条消息了
6 可靠消息最终一致
6.1 传统事务消息实现
传统事务消息实现,一种思路是依赖于 AMQP 协议用来确保消息发送成功,AMQP 模式下 需要在发送在发送事务消息时进行两阶段提交,首先进行 txselect 开启事务,然后再 进行消息发送,最后进行消息的 commit 或者是 rollback。这个过程可以保证在消息发 送成功的同时本地事务也一定成功执行,但事务粒度不好控制,而且会导致性能急剧下 降,同时依然无法解决本地事务执行与消息发送的原子性问题。
还有另外一种思路,就是通过保证多条消息的同时可见性来保证事务一致性。但是此类 消息事务实现机制更多的是用到 consume-transform-produce 场景中,其本质还是用来 保证消息自身事务,并没有把外部事务包含进来。
6.2 RocketMQ 事务消息
RocketMQ 事务消息设计则主要是为了解决 Producer 端的消息发送与本地事务执行的原 子性问题,RocketMQ 的设计中 broker 与 producer 端的双向通信能力,使得 broker 天生可以作为一个事务协调者存在;而 RocketMQ 本身提供的存储机制,则为事务消息 提供了持久化能力;RocketMQ 的高可用机制以及可靠消息设计,则为事务消息在系统在 发生异常时,依然能够保证事务的最终一致性达成。
6.3 可靠消息最终一致的实现
RocketMQ 4.3 以后,增加了对分布式事务的支持,就是将事务的执行状态保存在 RocketMQ 中,由 RocketMQ 去负责将 commit 状态的消息推送给下游系统
事务消息作为一种异步确保型事务, 将两个事务分支通过 MQ 进行异步解耦,RocketMQ 事务消息的设计流程同样借鉴了两阶段提交理论,整体交互流程如下图所示
- 事务发起方首先发送 prepare 消息到 MQ
- 在发送 prepare 消息成功后执行本地事务
- 根据本地事务执行结果返回 commit 或者是 rollback
- 如果消息是 rollback,MQ 将删除该 prepare 消息不进行下发,如果是 commit 消 息,MQ 将会把这个消息发送给 consumer 端
- 如果执行本地事务过程中,执行端挂掉,或者超时,MQ 将会不停的询问其同组的其 它 producer 来获取状态
- Consumer 端的消费成功机制有 MQ 保证