持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情
一: XA协议
我们常见的数据库连接事务中的 XA 是指由 X/Open 组织提出的分布式事务处理的规范. XA 规范主要定义了事务管理器(Transaction Manager)和局部资源管理器(Local Resource Manager)之间的接口.需要数据库厂商对此协议的实现才支持 我们常用的oracle和mysql 高版本都对其进行了实现,在我们出现分库分表时或者一个请求涉及到多个服务节点,则面临一个事物操作多个表多个库。
- AP:Application, 应用程序
- RM: Resource Manager,表示资源管理器
- TM: Transaction Manager,表示事务管理器,事务协调者
XA协议包括两阶段提交(2PC)和三阶段提交(3PC)两种实现。
2PC提交
数据库支持的 2PC【2 phase commit 二阶提交】,又叫做 XA Transactions。
MySQL 从 5.5 版本开始支持,SQL Server 2005 开始支持,Oracle 7 开始支持。其中,XA 是一个两阶段提交协议,该协议分为以下两个阶段:
具体步骤
1、准备阶段
- TM先发送个prepare消息给各个数据库,让各个库先把分布式事务里要执行的各种操作,各个数据库会准备好随时可以提交或者是回滚
- 各个数据库都返回一个响应消息给事务管理器,如果成功了就发送一个成功的消息,如果失败了就发送一个失败的消息
2、提交阶段
- 第一种情况,TM发现某个数据库告诉他说,我这儿失败了 把本地的那个事务回滚,然后各个库都回滚好了以后就通知TM,TM就认为整个分布式事务都回滚了
- 第二种情况,TM接收到所有的数据库返回的消息都是成功 提交好了通知下TM,所有数据库的事务都提交成功了通知TM,TM就认为整个分布式事务都执行成功 了
缺陷
- 同步阻塞:在阶段一里执行prepare操作会占用资源,一直到整个分布式事务完成,才会释放资源
- 单点故障TM是个单点,如果TM在第二阶段出现故障,那么RM会一直锁定
- 脑裂问题:在阶段二中,如果发生了脑裂问题,那么就会导致某些数据库没有接收到commit消息,有些库收到了commit消息,结果有些库没有收到
3PC提交
针对2PC做的一个改进,主要就是为了解决2PC协议的问题
具体步骤
1.CanCommit阶段(询问阶段)
- TM发送一个CanCommit消息给各个数据库,然后各个库返回个结果,不会执行实际的SQL语句的 就是各个库看看自己网络环境啊,各方面是否ready,这个阶段会有超时中止机制
2. PreCommit阶段(提交阶段)
- 会执行各个SQL语句,只是不提交
- 如果有个库对CanCommit消息返回了失败,TM发送abort消息给各个库,取消分布式事务
3. DoCommit阶段(提交或回滚阶段)
- PreCommit阶段都返回了成功,那么发送DoCommit消息给各个库,提交事务,各个库如果都返回提交成功给TM,那么分布式事务成功
- 如果有个库对PreCommit返回的是失败,那么TM认为分布式事务失败,直接发abort消息给各个库,回滚,各个库回滚成功之后通知TM,分布式事务回滚成功
缺陷
如果TM在DoCommit阶段发送了abort消息给各个库,结果因为脑裂问题,某个库没接收到abort消息,自己还因为超时机制的执行了commit操作
总结
- 不管是2PC还是3PC 都是数据一致性解决方案的实现
- XA模式是分布式强一致性的解决方案,但性能低而使用较少
二:基于可靠性消息的最终一致性方案
基于可靠消息的最终一致性是互联网公司常用的分布式数据一致性解决方案。它主要用消息中间件的可靠性机制来实现数据一致性的投递。 现在成熟的解决方案:RocketMQ 提供了事务消息模型。
概念介绍
- 事务消息:消息队列RocketMQ版提供类似XA或Open XA的分布式事务功能,通过消息队列RocketMQ版事务消息能达到分布式事务的最终一致。
- 半事务消息:暂不能投递的消息,生产者已经成功地将消息发送到了消息队列RocketMQ版服务端,但是消息队列RocketMQ版服务端未收到生产者对该消息的二次确认,此时该消息被标记成“暂不能投递”状态,处于该种状态下的消息即半事务消息。
- 消息回查:由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,消息队列RocketMQ版服务端通过扫描发现某条消息长期处于“半事务消息”时,需要主动向消息生产者询问该消息的最终状态(Commit或是Rollback),该询问过程即消息回查。
具体流程
事务消息发送步骤如下:
- 生产者将半事务消息发送至消息队列RocketMQ版服务端。
- 消息队列RocketMQ版服务端将消息持久化成功之后,向生产者返回Ack确认消息已经发送成功,此时消息为半事务消息。
- 生产者开始执行本地事务逻辑。
- 生产者根据本地事务执行结果向服务端提交二次确认结果(Commit或是Rollback),服务端收到确认结果后处理逻辑如下:
- 二次确认结果为Commit:服务端将半事务消息标记为可投递,并投递给消费者。
- 二次确认结果为Rollback:服务端将回滚事务,不会将半事务消息投递给消费者。
- 在断网或者是生产者应用重启的特殊情况下,若服务端未收到发送者提交的二次确认结果,或服务端收到的二次确认结果为Unknown未知状态,经过固定时间后,服务端将对消息生产者即生产者集群中任一生产者实例发起消息回查。
事务消息回查步骤如下:
- 生产者收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
- 生产者根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤4对半事务消息进行处理。
如果消费者没有签收该消息,那么消息队列服务会重复投递。
总结
- 降低业务系统与消息系统之间的耦合
- 一次消息发送需要两次网络请求 (half 消息 + commit/rollback 消息)
- 业务处理服务需要实现消息状态回查接口
三:基于本地消息表分布式事务
基于事务消息的模式对 MQ 系统要求较高,并不是所有 MQ 系统都支持事务消息的,RocketMQ 是目前为数不多的支持事务小的 MQ 系统。如果所依赖的 MQ 系统不支持事务消息,那么可以采用本地消息的分布式模式。
该种模式的核心思想是事务的发起方维护一个本地消息表,业务执行和本地消息表的执行处在同一个本地事务中。业务执行成功,则同时记录一条“待发送”状态的消息到本地消息表中。系统中启动一个定时任务定时扫描本地消息表中状态为“待发送”的记录,并将其发送到 MQ 系统中,如果发送失败或者超时,则一直发送,知道发送成功后,从本地消息表中删除该记录。后续的消费订阅流程则与基于事务消息的模式雷同。
四: 最大努力通知型
具体流程
最大努力通知和基于可靠性消息的最终一致性方案的实现是类似的。它是一中比较简单的柔性事务解决方案。使用于对数据一致性要求不高的场景。最典型的如:支付宝支付结果通知:
下面摘自支付宝支付文档接口说明
- 程序执行完后必须打印输出“success”(不包含引号)。如果商户反馈给支付宝的字符不是 success 这 7 个字符,支付宝服务器会不断重发通知,直到超过 24 小时 22 分钟。一般情况下,25 小时以内完成 8 次通知(通知的间隔频率一般是:4m,10m,10m,1h,2h,6h,15h);
从上述流程可以发现,所谓的最大努力通知,就是在商户端没有返回一个消息确认时,支付宝会不断进行重试,直到收到一个消息或者达到最大重试次数。 除了最大努力通知,支付宝还提供了一个供给商户主动回查的接口。
与可靠消息最终一致性的区别
可靠消息最终一致性:发起通知方需要保证将消息发出去,并且将消息发到接收通知方,消息的可靠性关键由发起通知方来保证。 最大努力通知:发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方。
五 : Seata AT
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。
这里主要介绍AT模式。
- TM:事务发起者。用来告诉TC全局事务的开始,提交,回滚。
- RM:事务资源,每一个RM都会作为一个分支事务注册在TC。
- TC:事务协调者,即独立运行的seata-server,用于接收事务注册,提交和回滚。
前提
- 基于支持本地 ACID 事务的关系型数据库。
- Java 应用,通过 JDBC 访问数据库。
执行流程
- TM 向 TC 申请开启一个全局事务,全局事务创建并生成一个全局唯一的XID。
- XID 在微服务调用链路的上下文中传播。
- RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖。
- TM 向 TC 发起针对 XID 的全局提交或回滚决议。
- TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
工作机制
一阶段:
业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。 在一阶段,Seata 会拦截“业务 SQL”,首先解析 SQL 语义,找到“业务 SQL”要更新的业务数据, 在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据,在业务数据更新之后, 再将其保存成“after image”,最后生成行锁。 以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
{
"branchId": 641789253,
"undoItems": [{
"afterImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "GTS"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"beforeImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "TXC"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"sqlType": "UPDATE"
}],
"xid": "xid:xxx"
}
二阶段:
二阶段如果是提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。 二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。 回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”, 如果两份数据完全一致就说明没有脏写,可以还原业务数据, 如果不一致就说明有脏写,出现脏写就需要转人工处理。
脏数据需手动处理,根据日志提示修正数据或者将对应undo删除(可自定义实现FailureHandler做邮件通知或其他)
总结常见的使用场景:
- 2PC/3PC:依赖于数据库,能够很好的提供强一致性和强事务性,但相对来说延迟比较高,比较适合传统的单体应用,在同一个方法中存在跨库操作的情况,不适合高并发和高性能要求的场景。
- 本地消息表/MQ 事务:都适用于事务中参与方支持操作幂等,对一致性要求不高,业务上能容忍数据不一致到一个人工检查周期,事务涉及的参与方、参与环节较少,业务上有对账/校验系统兜底。
- seata AT: 无侵入的分布式事务解决方案,适用于不希望对业务进行改造的场景,几乎0学习成本。
- 最大努力通知型:是最简单的一种柔性事务,适用于一些最终一致性时间敏感度低的业务,且被动方处理结果不影响主动方的处理结果。典型的使用场景:如银行通知、商户通知等。