分布式架构-事务-分布式事务
分布式事务
常见的分布式事务解决方案这里简单的列出表格说明, 建立一个整体的概念, 后详细说明每种方案
| XA | TCC | SEGA | AT | 可靠消息队列 | |
|---|---|---|---|---|---|
| CP/AP | CP | AP | AP | AP | AP |
| 一致性 | 强一致性 | 最终一致性 | 最终一致性 | 最终一致性 | 最终一致性 |
| 侵入性 | 无 | 强 | 中 | 无 | 强 |
| 隔离性 | 完全隔离 | 基于资源预留隔离 | 无隔离 | 基于全局锁隔离 | 无隔离 |
| 性能 | 差 | 非常好 | 非常好 | 好 | 非常好 |
CP事务-XA
XA事务架构中, 定义了一个全局的事务管理器(Transaction Manager-MT, 定义全局事务的范围, 开启全局事务, 提交或回滚全局事务), 和局部的资源管理器(Resource Manager-RM, 用于驱动本地事务), 以及一个事务协调者(Transaction Coordinator-TC, 维护全局和分支事务的状态, 协调全局事务提交和回滚), 这个职责划分机制在后面事务中同样存在, 后面不再重复介绍.

XA事务流程
XA事务将事务的提交过程分成了两步: 准备阶段和提交阶段

- 准备阶段: 协调者询问事务的所有准备者是不是准备好提交了, 准备好提交的回复Prepared, 否则回复No-Prepared. 这里的准备操作就是已经完成了redo log中对于修改的写入, 只是还没有写入Commit Record.
- 提交阶段: 收到了所有参与者的Prepared回复以后, TM先持久化自己的Commit状态, 然后通知所有的RM写入Commit Record也就是完成提交. 如果收到了一个No-Prepared或者超时了, TC将TM事务状态设置成Abort, 向所有的RM发送Abort命令, RM控制数据源执行回滚操作.
XA事务的问题和缺点
- 如果最后一步通知所有的分支事务提交的这一个过程中出错了, 存在分支事务没有被通知到, XA事务是察觉不了的, 仍然有可能出现不一致的问题, 但是可能性很小, 因为最后的分支事务的Commit操作实际上就是向redo log里面写入一条Commit Record, 是很轻量级的操作
- 单点问题: 如果参与者宕机了, TC有超时机制处理, 但是如果TC宕机了, 所有的参与者都会被影响, 如果一直没有收到Commit或Rollback命令, 就会一直等待
- 并发性能差: XA事务在准备阶段会锁定数据库资源, 等待第二阶段才能释放. 并且这个锁定数据库资源依赖数据源本身的实现
- 木桶效应: XA事务在第一阶段必须等待最慢的参与者完成准备才能进入到提交阶段
XA事务的优点
- XA事务对于事务的实现很大程度上依赖数据源本身对于事务的实现, 所以代码的侵入性很低, 在Seata中更是实现了无侵入性
- 事务强一致性, 满足ACID原则
XA事务的适用场景
有XA事务的优点和缺点能得出:
XA事务适用于单服务多数据源, 需要强一致性, 高隔离性, 低并发的场景
AP事务-TCC
TCC是Try Confirm Cancel的缩写, 这也是TCC事务的执行的流程. TCC事务不同于XA事务基于数据源本身的事务来实现, TCC事务通过冻结资源的形式来实现. 比如说一个TCC事务要保证扣除货款 + 扣除库存 + 发货的一致性, 在XA事务中是先在每个数据源中分别执行事务但是不提交, 最后统一提交.
TCC事务中不依赖数据源的事务, 它会直接将(Try阶段) 要扣除的货款, 库存, 货物冻结了. (Confirm)在参与者都冻结成功了以后, 执行后续的业务处理, 如果都完成了, 事务结束, 如果有步骤失败, 会循环重复执行, 即最大努力交付, 如果有一方业务不可行或者是循环超时了, 这个时候就进入到Cancel阶段执行解冻操作, Cancel解冻操作也是最大努力交付.
TCC的流程
- Try阶段: 将多个业务要用到的资源先预留出来, 也就是将它们冻结了, 所有的Try操作都成功了, 进入到Confirm阶段, 否则进入到Cancel阶段.
- Confirm阶段: 使用预留的资源循环执行续的所有的业务处理操作, 即最大努力交付, 如果有一方业务不可行, 或者循环超时了, 进入到Cancel阶段, 否则事务结束.
- Cancel阶段: 同样是最大努力交付, 循环将所有的预留资源释放

TCC的优点
- TCC是实现在用户代码上的, 不依赖于数据库这种基础设施, 有较高的灵活性
- TCC有强隔离性: 常用于解决超售问题, 即两个客户在短时间内同时购买了一件商品, 各自购买的数量不超过目前库存, 但是购买数量总和超过了库存数量. 通过TCC在Try阶段会将资源冻结, 这样就保证了隔离性, 第一个客户冻结了库存以后, 第二个顾客它的Try阶段就会执行失败
TCC的缺点
- TCC代码是实现在用户代码上带来的问题就是具有强侵入性, 一般来说会依托于分布式事务的中间件来减少一些公共编码部分, 但是强侵入性带来的更大的问题是, 不是所有情况我们都能侵入, 比如我们这里的货款是直接扣减银行账户的余额, 这个时候我们无法让银行给我们实现一个冻结余额的服务, 也就无法使用TCC事务了
- 空回滚问题: 如果在Try阶段失败了, 这个时候要执行回滚, 但是不是所有资源都被预留冻结了, 没有被冻结的资源也会被解冻, 这就是空回滚问题, 解决方式就是在TM记录每个分支事务的状态, 如果是在Try阶段就不会滚
- 业务悬挂问题: 这个问题基于空回滚, 如果在Try阶段, 存在命令时延了, 最终导致超时, Try阶段执行失败, 这个时候执行回滚. 回滚完以后, Try命令到达, 这个时候就会有分支事务执行了冻结操作, 并且永远不会被解冻了. 解决方式同样是记录分支事务的状态, 如果不是在Try阶段, 不执行冻结操作.
TCC事务适用的场景
能够侵入编码, 对性能有一定的要求, 有非关系型数据库参与的事务(非关系型数据库不支持XA), 并且有高隔离性需求的业务
AP事务-可靠消息队列
这个做法实际上就是我们常见的通过MQ来保证多个关联服务都能成功执行的方案. 这个方案的关键就在于怎么保证消息队列是可靠的, 并且能在失败的时候重试, 方案本身实现简单, 我调用完账号服务扣减了货款以后, 将这个事务的消息发送给消息队列, 然后消费端消费信息即可, 这种方案是很明显的侵入性拉满了的.

实现的关键
这个方案最大的问题就是, 我们怎么保证消息队列是可靠的并且能够不断失败重试.
对于消息生产方, 如何保证消息生产的可靠性以及失败重新发送的功能
创建一个Task表, 在发送每个消息队列的消息之前, 将消息持久化(这个时候状态时created), 并将消息发送出去. 如果被生产端正常消费了, 消费端修改表为completed, 否则设置成failed.
接下来通过一个定时job, 不断扫描Task表中的不是competed的消息并重新发送.
怎么保证消费的幂等性
上面的失败重试的方案, 我们需要保证我们的消息的幂等性, 不然就会出现消费端消费了多条相同的消息造成错误了.
我们通过全局唯一ID来保证每条消息消费的幂等性
可靠消息队列的优缺点
优点:
- 理解起来简单, 编码简单
- 在现在的现有MQ框架下, 重发的机制能更简单的实现
缺点:
- 同样有单点问题, 你MQ炸了, 就真的全炸了. 和TC是一样的问题, 解决的方案也很简单了, 上集群.
- 不保证隔离性, 纯依靠程序员的实现, 如果有两条消息都要修改同一条记录, 我们甚至不太知道执行顺序, 需要引入额外的机制保证隔离性(一定程度上依靠数据源本身的隔离性).
AP事务-SEGA事务
SEGA事务引入了长事务机制, 将一个大的事务切分成若干个小的事务($T_i$), 然后针对每个小的事务设计一个补偿动作($C_i)$.这个补偿动作和切分的小事务满足
- 两个操作都要幂等性
- 交换律, 两个动作交换了顺序执行的结果不变
- 补偿动作一定要执行成功, 不存在执行失败的场景, 必须最大努力交付, 或者人工介入
SEGA事务的流程
如果$T_1$到$T_i$都执行成功了, 事务就是成功完成了, 否则就采取下面两种恢复策略
- 正向恢复(事务继续提交): 如果$T_i$失效了就重复重新提交到$T_i$成功, $T_1$,$T_2$,…,$T_i$(失败),$T_i$(重试)…,$T_{i+1}$,…,$T_n$。适用于事务最终一定要成功的场景, 你扣了人家的货款, 东西就一定要发出去.
- 反向恢复(事务回滚): 如果$T_i$失败了, 就开始执行$C_i$对$T_i$进行补偿, 直到成功, 这里一定要保证$C_i$的成功. 反向恢复的执行模式为:$T_1$,$T_2$,…,$T_i$(失败),$C_i$(补偿),…,$C_2$,$C_1$。
SEAG优点
- 补偿操作往往比冻结操作实现起来简单得多, 可实现性也高很多
- 无锁, 性能好, 吞吐高
SEGA缺点
- 时效性差
- 没有隔离性, 会有脏写的问题
- 有侵入性, 需要编写回滚操作, 还要保证幂等
适用场景
SEGA基本只有一些遗留业务在使用, 适用于业务流程长, 业务流程多的场景, 并且无法提供TCC模式要求的三个接口的情况下.
AP事务-AT
接下来就是我们伟大的AT事务, 也是Seata的默认的分布式事务模式.
AT事务同样是分阶段提交的事务模型, 不过弥补了XA事务中资源锁定周期过长的缺点(XA事务里面只有在所有的子事务都执行完毕以后才能Commit释放锁)
实际上就是将XA事务的事务模型从必须统一Commit变动成了可以有子事务先Commit, 但是要记录在TM中的Undo Log中, 如果事务要执行回滚了, 把Undo-Log删除了, 然后根据Undo-Log恢复到更新前就好了
AT事务的流程

这里的Undo Log通过拦截SQL语句, 在执行完修改语句前对要修改的数据拍个快照. 如果执行失败了, 就通过Undo-Log执行恢复操作. 其实可以看作利用SEGA事务补偿操作的思路来优化的XA事务的结果
AT事务的优点
- 完美的无侵入性, 不需要改一行代码.
- 性能较好, AT事务不需要一直持有锁, 局部事务会在执行成功以后直接Commit释放锁
AT事务的缺点
- 伟大的AT事务也带来了脏写问题, 我们能发现AT模式的关键实现快照会导致脏写问题, 如果有两个事务同时要修改同一行数据, 第二个事务修改完以后, 第一个事务根据快照回滚了, 这个时候就出现了脏写问题.
- 解决方法就是引入全局锁, 在执行全局事务之前对要修改的数据加上全局事务锁, 只有持有锁的全局事务具有修改权(其实就是个全局的写锁)
AT事务的适用场景
基于关系型数据库的大多数场景都可以(毕竟是0侵入性)
