DTP和XA
分布式事务的解决方案之一就是两阶段提交协议(2PC:Two-Phase Commit)
1994年X/Open组织(现在的Open Group)定义了分布式事务处理的DTP模型,该模型包括这样几个角色:
- 应用程序(AP):业务微服务
- 事务管理器(TM):全局事务管理者
- 资源管理者(RM):一般是数据库
- 通信资源管理器(CRM):是TM和RM间的通信中间件
在该模型中,一个分布式事务(全局事务)可以被拆分成多个本地事务,运行在不同的AP和RM上,每个本地事务的ACID很好实现,但是全局事务必须保证其中包含的每个本地事务都能成功,若有一个失败,则所有其他事务都必须回滚。
问题是本地事务处理过程中,并不知道其他事务的运行状态,因此就需要通过CRM来通知各个本地事务,同步事务执行状态。
因此各个本地事务的通信必须有一个统一的标准,否则不同数据库之间就无法通信,XA就是X/Open DTP中通信中间件和TM间联系的接口规范,定义了用于通知事务开始、提交、终止、回滚等接口,各个数据库厂商都必须实现这些接口。
2PC(两阶段提交)
两阶提交协议就是根据这一思想衍生出来的,将全局事务拆分为两个阶段来执行。
- 阶段一:准备阶段,各个本地事务完成本地事务的准备工作。
- 阶段二:执行阶段,各个本地事务根据上一阶段执行结果,进行提交或回滚。
这个过程中需要一个协调者(coordinator),还有事务的参与者(voter)。
正常情况
投票阶段:协调组询问各个事务参与者,是否可以执行事务。每个事务参与者执行事务,写入undo和redo log,然后反馈事务执行成功。(agree)
提交阶段:协调组发现每个参与者都可以执行事务(agree),于是向各个事务参与者发出commit指令,各个事务参与者提交事务。
异常情况
投票阶段:协调组询问各个事务参与者,是否可以执行事务。每个事务参与者执行事务,写入undo和redo log,然后反馈事务执行结果,但只要有一个参与者返回的是Disagree
,就说明执行失败。
提交阶段:协调组发现每个有一个或多个参与者返回Disagree,认为执行失败,于是向各个事务参与者发出abort
指令,各个事务参与者回滚事务。
缺陷
-
存在CRM单点故障问题
协调者挂了之后就不知道别人什么情况。
-
阻塞问题
在准备阶段,提交阶段,每个事务参与者都会锁定本地资源,并等待其他事务的执行结果,阻塞时间较长。
总结
- 面对二阶段提交的缺点,后来又演变出来了三阶段提交,但是依旧没有解决阻塞和资源锁定的问题,而且引入了新的微,因此实际场景使用较少。
- 2PC拥有稳定成熟的框架支持,可以保证强一致,并且对代码侵入度不高,
使用场景
- 适用于对效率要求不高,一致性要求高的场景
- 代码侵入度小,不影响原有代码
3PC(三阶段提交)
TCC
TCC模式可以解决2PC中资源锁定和阻塞的问题,减少资源锁定时间。
基本原理
本质是一种补偿的机制,事务运行包括三个方法,
- try:资源的检测和预留
- confirm:执行业务操作提交;要求try成功confirm一定能成功
- cancel:预留资源释放
执行分两个阶段:
- 准备阶段(try):资源的检测和预留
- 执行阶段(confirm/cancel):根据上一步的结果,判断下面的执行方法。如果上一步所有事务参与者都成功,这里执行confirm,否则执行cancel
看似与两阶段提交没有什么区别,其实差距很大。
- try,confirm,cancel都是独立的事务,不受其他参与者的影响,不会阻塞等待他人。
- try,confirm,cancel由程序员在业务层编写,锁粒度由代码控制。
实例
假如账户A原来有100元,需要从余额扣除30元。如图:
- 一阶段(Try):余额检查,并冻结用户部分金额,此阶段执行完毕,事务已提交。
- 检查用户余额是否足够,如果足够,冻结部分余额
- 在账户表中添加冻结金额字段,值为30,余额不变
- 二阶段
- 提交(Confirm):真正的扣款,把冻结金额从余额中扣除,冻结金额清空。
- 修改冻结金额=0,修改余额为100-30=79
- 补偿(Cancel):释放之前冻结的金额,不是回滚
- 余额不变,修改账户冻结金额=0
- 提交(Confirm):真正的扣款,把冻结金额从余额中扣除,冻结金额清空。
优势&缺点
优势
- 避免资源长期锁定和阻塞等待操作。执行效率高。
TCC执行的每个阶段都是独立的事务,不需要等待其他事务的结果。如果执行失败,不是执行的回滚而是执行的补偿。
缺点
- 代码侵入:需要人为编写try,confirm,cancel操作,对开发人员要求高
- 开发成本高:一个业务需要拆分三个阶段,开发成本高
- 安全性考虑:cancel动作如果执行失败,资源就无法释放。需要引入重试,但是引入重试又有可能造成重复执行问题,还需要考虑重试时的幂等问题。
使用场景
- 对事务有一定的一致性要求(最终一致性)
- 对性能要求较高
- 开发人员具备较高的编码能力和幂等处理经验
可靠消息服务
源于eBay,设计思想是将远程分布式事务拆分成一系列的本地事务。
基本原理
事务分为发起者A
和参与者BCD
- 事务
发起者A
执行本地事务 - 事务
发起者A
通过MQ将需要执行的事务信息发送给事务参与者BCD
- 事务
参与者BCD
接收到消息后执行本地事务
现实场景(老师给的场景,好像不恰当):
- 你点了一杯奶茶,付完钱。(发起者A完成)
- 服务员给了你一个号码牌。(参与者B)
- 你凭这张小票,一定能够领到一杯奶茶。(不断问服务员,重试)
本地消息表
为了避免消息发送失败或丢失,消息需要持久化到数据库。实现有简化版本和解耦合版本两种。
简化版本
- 事务发起者
- 开启本地事务
- 执行业务操作
- 发送消息到MQ
- 保存消息记录到数据库,记录为已发送
- 提交本地事务
- 事务参与者
- 接收消息
- 开启本地事务
- 执行业务操作
- 修改数据库记录状态为已消费
- 提交本地事务
- 额外的定时任务
- 定时扫描数据库中超时未消费的,重新发送
优点&缺点
优点
- 与TCC相比,不用写那么多代码。
缺点
- 数据一致性完全依赖MQ,因此消息服务一定需要可靠。
- 需要处理被动业务方的幂等问题。
- 被动业务失败不会主动业务回滚,而是重试被动的业务。
- 事务业务与消息业务耦合,发起者业务完成后需要写消息表。
独立消息服务
通过引入独立的消息服务,完成对消息的持久化、发送、确认、失败重试等行为。
RocketMQ事务消息
自带事务消息,可以保证消息的可靠性,原理就是自带了一个本地消息表。
RabbitMQ的消息确认
RabbitMQ确保消息不丢失使用的是ACK确认机制。
- 生产者确认机制:确保从生产者 -> MQ不丢失
- 生产者发送消息到mq,设置异步监听MQ的ACK
- MQ接收到消息,返回ACK到生产者
- 消息到exchange但是route失败,返回失败ACK
- 消息route成功,持久化失败,返回失败ACK
- 消息route成功,持久化成功,返回成功ACK
- 生产者提前编写好不同回调的处理方式
- 成功,记录日志
- 失败,一段时间后重新发送
- 消费者确认机制:确保从MQ -> 消费者正确消费
- 消费者开启监听时开启手动ACK模式
- mq将消息发送到消费者后,会等待消费者ACK
- 收到消费者ACK才会删除消息
- 一直没有收到ACK则一直保存
- 消费者断开或异常后,发送给其它消费者(此处需要做好幂等)
- 消费者处理完消息,提交事务,手动ACK。如果异常,则不会ACK,业务处理失败。等待处理下一条消息。
事务消息优点&缺点
- 优点
- 业务简单,不需要写三个阶段的代码
- 多个本地事务,缩短资源锁定时间,性能高
- 缺点
- 代码侵入度高
- 一致性依赖MQ的可靠性
- 消息发起者可以回滚,参与者无法引起全局事务回滚
- 时效性差,完全取决于MQ的消息发送是否及时,消费是否及时。
针对缺点3,无法回滚问题。有人提出可以让事务参与者失败后,利用MQ通知消息服务,由消息服务通知其他参与者回滚,可是这是在利用MQ重新实现了2PC,又是一个轮子。没必要
Seata
Seata是阿里开源的框架,支持XA,AT,TCC,SAGA四种模式。
AT模式
2019年1月份,Seata开源了AT模式。AT模式是一种无侵入的分布式事务解决方案。是对TCC或两阶段提交模型的优化,解决了TCC模式代码侵入,编码复杂等问题。
在AT模式下,用户只需要关注自己业务的"sql",用户的业务SQL作为“一阶段”,Seata框架会解析SQL自动生成事务的“二阶段”提交或回滚操作。
Seata官方文档:seata.io
乍一看和TCC协议很像,都是分两阶段提交。
- 一阶段:执行本地事务,并返回执行结果
- 二阶段:根据一阶段的结果,决定执行提交或回滚
但是AT模式底层完全不同,二阶段完全不需要我们编码,框架帮我们实现了。我们写的代码和本地事务代码完全一致,无需手动处理分布式事务。
一阶段
一阶段,Seata会拦截业务SQL
,首先解析SQL语义,找到业务SQL
要更新的业务数据,在业务数据被更新前,将其保存为before image
,然后执行业务SQL
,更新业务数据,在业务数据执行之后,再将其保存为after image
,最后获取全局行锁,提交事务。以上所有操作都是在一个本地事务中完成,保证了一阶段的原子性。
before image
和after image
类似于数据库的undo log和redo log。
二阶段提交
二阶段如果是提交的话,因为业务SQL
在一阶段已经提交到数据库,所以Seata框架只需要将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
二阶段回滚
回滚可直接使用before image
还原业务数据,但在还原前首先要检查脏写,对比数据库当前业务数据
与after image
,如果两份完全一致则没有脏写,可以还原;如果数据不一致,说明出现了脏写,需要人工处理。
详细架构和流程
基本概念
- TC(Transaction Coordinator):事务协调者,维护全局和分支事务的状态,驱动全局事务提交或回滚(TM之间的协调者)
- TM(Transaction Manager):事务管理器,定义全局事务的范围:开启全局事务,提交或回滚全局事务
- RM(Resource Manager):资源管理器,管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚
- TM:业务模块中全局事务的开启者
- 向TC开启一个全局事务
- 调用其它微服务
- RM:业务模块执行者中,包含RM部分,负责向TC汇报事务执行状态
- 执行本地事务
- 向TC注册分支事务,并提交本地事务执行结果
- TM:结束对微服务的调用,通知TC,全局事务执行完毕,一阶段结束
- TC:汇总各个分支事务执行结果,决定分布式事务是提交还是回滚
- TC:通知所有RM提交/回滚资源,二阶段结束
详细流程
- 一阶段
- TM开启全局事务,并向TC声明全局事务,包括全局事务XID信息
- TM所在服务调用其他微服务
- 微服务,主要由RM执行
- 查询
before image
- 执行本地事务
- 查询
after image
- 生成
undo log
写入数据库 - 向TC注册分支事务,并告知事务执行结果
- 获取全局锁(防止其他全局事务并发修改当前数据)
- 释放本地锁(不影响其他业务对数据的操作)
- 查询
- 二阶段
- TC 统计各个分支事务的执行情况,根据情况做对应的操作
- 都成功:通知分支事务,提交事务
- 有分支失败:通知执行成功的事务,回滚数据(做补偿)
- 分支事务RM
- 提交事务:直接清空
before image
和after image
信息,释放全局锁 - 回滚事务
- 校验
after image
,判断是否有脏写 - 如果没有脏写,回滚到
before image
,清空before image
和after image
- 如果有脏写,需要人工干预
- 校验
- 提交事务:直接清空
- TC 统计各个分支事务的执行情况,根据情况做对应的操作
优点&缺点
- 优点
- 一阶段无锁,高性能
- 补偿服务易于实现
- 缺点
- 解析,查询和保存undo log的blob字段徒增性能损耗,根据二八定理,80%的业务是不会出现问题,而需要接受这部分写入log的损耗
- 不保证隔离性,一阶段完成后仅维护着全局事务锁XID提交了本地事务。
适用场景
- 业务流程长,业务逻辑多的地方。
- 历史遗留代码无法修改的情况