初探分布式事务
概括
最近在做的项目设计到一些金钱交易方面的事情。涉及到一些分布式事务的逻辑。
参考了一些资料,做一些总结。
- 一句话概括分布式事务:
一部分资源的数据更新以后,怎么保证另外一部分资源的数据也必须更新成功。
资源的概念可以是mysql,也可以是redis,mongodb这样的Nosql,甚至是消息队列,文件系统等等
- 一句话总结分布式事务的核心思想:
将分布式事务分拆为本地事务
所以当资源无法完成本地事务时,分布式事务就无法成立。
分布式事务的3种方案:
两阶段提交
ebay的本地消息表
事务消息
2阶段提交
两阶段提交作为一种强一致性协议,在1994年就有了标准分布式事务规范:
DTP/XA规范(Distributed Transaction Processing)
这个规范中定义的XA protocol作为标准2阶段提交协议已经有了各种实现。
通用实现
角色分为(全局)事务管理器TM和(局部)资源管理器RM,以及上层业务
1 业务发送update 1和update 2给TM
TM失败或超时,事务失败
2 TM生成事务ID
TM向1所在的RM1发送update指令
TM向2所在的RM2发送update指令
RM1和RM2暂存(可失缓冲区)但不执行
TM,RM失败或超时,事务失败
3 业务发送commit给TM
TM失败或超时,事务失败
TM向RM1发送prepare指令,RM1检查update语句是否报错后,RM1将update语句和prepare状态持久化后返回成功
TM向RM2发送prepare指令,RM2检查update语句是否报错后,RM2将update语句和prepare状态持久化后返回成功
任何一个RM失败,事务失败,此时的失败需要进行持久化记录
4 TM将事务ID和是否成功持久化
这里的持久化标志着事务是否真正成功
5 TM向业务返回成功或失败
TM返回信息未被业务成功接受,业务超时,事务的是否成功取决于第4阶段
6 事务成功:
TM向RM1发送commit指令
TM向RM2发送commit指令
事务失败:
TM向RM1发送rollback指令
TM向RM2发送rollback指令
每隔一段时间,TM向RM1和RM2查询处于prepare状态的事务ID
TM挂掉以后重启,TM立即向RM1和RM2查询处于prepare状态的事务ID。
对于每个事务ID,RM1和RM2的事务ID列表很有可能是不同的(有的prepare成功有的没有,或者有的已经commit或者rollback有的没有)
每个事务ID都要查询自己的持久化记录决定成功或失败重新发送commit或者rollback
如果TM不存在这个事务ID,说明事务ID持久化出错了,那这个事务也是错误的需要rollback事务
mysql的XA协议
协议分为几个关键词
1 | xa start 'id'; |
当xa prepare成功写入binlog以后,即使TM挂掉,也可以通过xa recover查询处于prepare状态的事务ID
因此可以很方便的实现二阶段提交
mysql在今年的更新修复了一个长达10年的bug,使得prepare状态的XA事务被持久化不再自动rollback
具体看这里
MySQL · 特性分析 · 浅谈 MySQL 5.7 XA 事务改进
二阶段提交虽然是强一致性,但是由于xa commit的不同节点的顺序问题,还是有可能出现脏读的,这里有一些相关讨论
ebay的本地消息表
ebay的一个架构师在Base:An Acid Alternative中提出的方法
例如A给B转账100这种场景。A,B分属RM1和RM2两个模块
1 业务向RM1提交请求,RM1向RM2提交一个消息请求,RM2生成一个消息ID例如Xid,将它持久化以后,返回这个Id
1 | insert into message(id, status) values ('Xid','prepare') |
此时Xid在RM2中是prepare阶段
2 RM1提交扣款事务,RM1操作的SQL有两张表,一张是用户余额表,一张是消息表
用户余额表进行扣费,消息表包含事务ID,交易双方和交易金额
1 | Begin transaction |
3 RM1向业务返回成功(延迟到账)
4 RM1根据第2阶段的结果向RM2进行commit或rollback
5 RM2收到请求,执行本地事务
1 | Begin transaction |
RM2需要不断扫描prepare状态的消息,向RM1进行查询。这种prepare状态的事务是因为第4阶段请求失败导致的。
第5阶段的执行保证了事务的幂等性,防止多次执行消息导致RM2的多次执行
事务消息
1 发送方向 MQ 服务端发送消息;
2 MQ Server 将消息持久化成功之后,向发送方 ACK 确认消息已经发送成功,此时消息为半消息。
3 发送方开始执行本地事务逻辑。
4 发送方根据本地事务执行结果向 MQ Server 提交二次确认(Commit 或是 Rollback),MQ Server 收到 Commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 Rollback 状态则删除半消息,订阅方将不会接受该消息。
5 在断网或者是应用重启的特殊情况下,上述步骤4提交的二次确认最终未到达 MQ Server,经过固定时间后 MQ Server 将对该消息发起消息回查。
6 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
7 发送方根据检查得到的本地事务的最终状态再次提交二次确认,MQ Server 仍按照步骤4对半消息进行操作。
优缺点总结
讨论了2阶段提交,本地消息表和事务消息三种工程上的分布式事务实现机制
下面分别做优缺点总结
- 2阶段提交
优点:
一旦向业务返回成功,就很少会失败需要人工干预校正,业务xa start到xa end前会对执行的sql进行校验
缺点:
1 同步阻塞性能差
2 虽然是理论上说是强一致性,但是工程实现上还是最终一致性,因为最终的commit还是基于异步提交,可能出现不一致的情况
- 本地消息表
优点:
最终一致性实现,对于简单的sql而言下游出错可能性很低。例如转账服务,一般只有A扣款需要余额校验,B加钱很难出错。
缺点:
1 消息耦合在数据库中
2 对于下游是复杂事务逻辑的时候,一旦出错就需要人工校正
- 事务消息
优点:
消息不耦合,对业务代码上实现友好(java直接封了库,对业务开发是透明的)
缺点:
引入了额外的消息中间件,对小业务或者支持不完善的语言(例如C++)来说不友好(需要额外掌握MQ知识)