分布式事务
分布式协议
- XA规范
XA(eXtended Architecture)标准是X/Open 组织针对分布式事务(DTP)处理的规范,它描述了全局事务管理器和本地资源管理器之间的接口,允许多个资源在同一分布式事务中访问。DTP 模型中包含一个全局事务管理器(TM,Transaction Manager)和多个资源管理器(RM,Resource Manager)。全局事务管理器负责管理全局事务状态与参与的资源,协同资源一起提交或回滚;资源管理器则负责具体的资源操作。
AP定义事务边界以及那些组成事务的特定于应用程序的操作
分布式事务方案
2PC(强一致性)
2PC即两阶段提交协议(Two Phase Commit),是对XA规范的实现,从字面意思理解就是将提交分为两个阶段:
- 准备阶段
TM通知各个RM准备提交它们的事务分支,如果RM本地事务执行成功,则返回成功;如果RM的本地事务执行失败,则返回失败。 - 提交阶段
如果TM收到所有分支事务返回成功的消息,则向每个RM发送提交消息;否则发送回滚消息。RM根据TM的指令执行提交或者回滚本地事务操作,释放本地事务处理过程所用的所有资源。
以创建订单业务场景为例分析2PC存在的问题
- 锁资源无法释放
当订单服务操作订单库和调用库存服务后,在准备阶段执行完成后,TM宕机了,此时数据库资源被占用,导致RM一直阻塞,无法释放锁资源 - 数据不一致
在提交阶段,TM给所有RM发送提交请求,如果某些RM由于网络抖动或者请求过程中协调者发生故障,只有部分RM收到了commit请求,接收到commit请求的RM会正常提交分支事务,没有收到commit请求的RM则无法执行事务提交操作,最后导致数据不一致
JTA/XA规范实现
以MySQL为例(MySQL实现了XA规范),模拟两个分支事务组成的分布式事务,伪代码:
- 创建两个资源管理器操作接口,分别代表两个分支事务,他们都拥有各自的资源。具体为XAConnection和XAResource。
- AP向TM请求创建一个分布式事务表示开始事务,并得到全局事务id。
- 分别执行两个分支事务
- 准备阶段:TM通过XAResource.prepare()询问两个分支事务,是否可以提交
- 提交阶段:如果两个分支事务的prepare()都返回0(成功)则通知各个RM提交事务,否则回滚。
seata AT模式
TCC(补偿事务/柔性事务)
TCC即Try-Confirm-Cancel
- Try:完成所有业务检查,预留必须的业务资源。注意判断:
a.幂等判断:通过查询日志记录,判断本次分布式事务Try逻辑是否已经有执行记录
b.悬挂处理:通过查询日志记录,判断本次分布式事务Confirm或者Concel是否又执行记录 - Confirm:真正执行的业务逻辑,不做任何业务处理,只是用Try阶段预留的资源。因此,Try能执行成功则Confirm必须能成功。注意Confirm操作的幂等判断
- Cancel:释放Try阶段预留的业务资源。同样Cancel操作也需要幂等判断
空回滚:如果Try操作没有执行,则Cancel操作也不能执行。
TCC分布式事务流程:
- 主业务服务开启本地事务
- 主业务流程获取分布式事务唯一id
- 主业务服务发起所有从业务服务的Try调用
- 当所有从业务服务的Try接口调用成功,主业务服务提交本地事务;若有失败,则本地事务回滚。
- 若主业务服务提交本地事务,则TCC模型分别调用所有从业务服务的Confirm接口;若主业务服务回滚本地事务,则调用所有从业务服务的Cancel接口
TCC开源框架
- Tcc-Transaction
- Hmily
- Seata TCC
- …
TCC demo
以转账为例,设计一个转账接口,假设A账户转账10元给B账户:
- A账户接口伪代码
try:
判断A账户余额是否大于10
减少10元
调用B账户增余额接口
Confirm:
Cancel:
增加10元
- B账户接口伪代码
try:
增加10元
Confirm:
Cancel:
减少10元
咋一看似乎逻辑很严谨没有什么问题,但是这里有很多的问题:
1.A账户Try操作没有做幂等判断和悬空判断
2.A账户Cancel操作没有幂等判断和空回滚处理
3.B账户Try操作增加10元,放在Confirm操作处理更简单,并且加上Confirm幂等处理
4.B账户Cancel操作为空
较为合理的伪代码:
- A账户接口伪代码
try:
幂等判断(判断Try操作本次事务是否已经执行)
悬挂处理(判断本次事务的Confirm或者Cancel操作是否已经执行过,如已经执行过就不能执行Try操作)
判断A账户余额是否大于10
减少10元
调用B账户增余额接口
Confirm:
空
Cancel:
幂等校验
判断Try操作是否执行过,如没有执行就不允许Cancel操作执行
增加10元
- B账户接口伪代码
try:
空
Confirm:
幂等校验
增加10元
Cancel:
空
小结
允许空回滚:由于网络丢包导致Try操作失败,必会触发Cancel操作,此时Cancel必须识别出这是空回滚,返回成功。
防悬挂:由于网络堵塞丢包,Try操作超时,此时分布式事务回滚触发Cancel操作,故出现Cancel比Try操作先执行。
幂等控制:Try/Confirm/Cancel都必须做幂等判断,防止重复执行。
可靠消息
本地消息表
- 用户注册
用户发送注册请求,user_service会将用户注册和赠送优惠券作为本地事务同时入库,保证原子性和一致性 - 定时任务
定时任务扫描本地消息日志表,将优惠券消息发送到mq,并在消息中间返回成功后删除本地日志表,否则等到下一次重试。 - 消息消费
优惠券服务监听mq消息,在接收到消息并处理成功后,使用mq的应答机制给mq发送ACK确认。此外定要保证消息消费的幂等性。
Rocketmq事务消息最终一致性
1.生产者将半消息发送个Rocketmq服务
2.Rocketmq服务收到半消息后,给生产者响应半消息发送成功;但此时的半消息只是保存起来不会被消费者消费
3.生产者收到成功后,执行本地事务
4.生产者根据本地事务执行结果向Rocketmq提交二次确认结果(提交或回滚)
- 提交:本地事务执行成功,提交二次确认结果,此时Rocketmq服务将半事务消息的"暂不能投递"状态改为"可投递"状态,并投递给消费者。
- 回滚:本地事务执行失败,回滚二次确认结果,将半事务消息改为不投递状态。
5.Rocketmq自动执行回查事务状态
6.生产者收到回查消息后,去检查本地事务是否执行成功
7.再次提交二次确认结果,参照4.