分布式详解5
分布式事务有哪些解决方案
基于XA协议的
两阶段提交和三阶段提交,需要数据库层面支持。
XA协议通常不是通过简单的单条SQL命令来完成的,而是通过一组命令/API调用来管理一个分布式事务的完整生命周期。这些命令通常由事务管理器 调用,而应用程序则通过高级抽象(如Java JTA)来间接使用。
一些核心命令:
| 段 | 命令/函数名 | 说明 | 常见实现举例 |
|---|---|---|---|
| 阶段一:准备阶段 | xa_start |
开启一个分布式事务分支,将其与一个全局事务ID(XID)关联。 | C API / 数据库驱动内部调用 |
xa_end |
结束当前线程与分布式事务分支的关联。 | C API / 数据库驱动内部调用 | |
xa_prepare |
核心命令。事务管理器向所有参与者发出此命令。参与者将事务内容持久化到日志中,并锁定相关资源,然后返回“就绪”或“失败”状态。 | MySQL: XA PREPARE '<xid>' |
|
| 阶段二:提交/回滚阶段 | xa_commit |
提交命令。如果所有参与者都返回“就绪”,事务管理器向所有参与者发出此命令,要求提交事务。参与者完成持久化操作并释放资源。 | MySQL: XA COMMIT '<xid>' |
xa_rollback |
回滚命令。如果任何参与者返回“失败”或超时,事务管理器向所有参与者发出此命令,要求回滚事务。参与者根据准备阶段的日志进行回滚并释放资源。 | MySQL: XA ROLLBACK '<xid>' |
|
| 恢复 | xa_recover |
事务管理器查询资源管理器,找回那些处于“准备完成”状态(即已PREPARE但未COMMIT/ROLLBACK)的分布式事务,用于故障恢复。 |
阶段提交示例
1. 两阶段提交流程
假设我们有一个全局事务,需要同时更新db1和db2中的账户。
第一步:在两个数据库连接上分别开启并执行事务
1 | -- 在连接1(负责账户A)上 |
第二步:准备阶段 - 事务管理器发出 PREPARE
1 | -- 在连接1上 |
此时,两个MySQL实例都将事务结果写入日志,并锁定相关行,进入PREPARED状态。
第三步:提交阶段 - 事务管理器发出 COMMIT 如果两个PREPARE都成功:
1 | -- 在连接1上 |
至此,分布式事务完成。
如果在第二步中任何一个PREPARE失败,或者在第三步前发生了故障,事务管理器则应向所有参与者发出:
1 | XA ROLLBACK '12345','1'; |
2. 相关监控命令
查看MySQL中处于
PREPARED状态的XA事务:1
XA RECOVER;
这条命令会输出所有已经准备但尚未提交或回滚的XA事务的XID信息。
基于事务补偿机制的
TCC,基于业务层面实现
简述TCC事务模型
TCC(补偿事务):Try、Confirm、Cancel
针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作
Try操作:做业务检查及资源预留Confirm操作:做业务确认操作Cancel操作:实现一个与Try相反的操作即回滚操作
执行流程:TM(事务管理器)首先发起所有的分支事务的try操作,任何一个分支事务的try操作执行失败,TM将会发起所有分支事务的Cancel操作;若try操作全部成功,TM将会发起所有分支事务的Confirm操作。其中Confirm/Cancel操作若执行失败,TM会进行重试。
特点与要求:
- TCC模型对业务的侵入性较强,改造的难度较大
- 每个操作都需要有
try、confirm、cancel三个接口实现 - TCC中会添加事务日志,如果
Confirm或者Cancel阶段出错,则会进行重试 Confirm和Cancel操作需要支持幂等- 如果重试失败,则需要人工介入进行恢复和处理
结合一个经典的 “订单 -> 支付 -> 积分” 场景,来详细讲解 TCC事务 的完整流程。
这个场景包含三个微服务:
- 订单服务:创建订单
- 支付服务:扣减账户余额
- 积分服务:增加用户积分
第一阶段:Try 阶段(业务检查与资源预留)
这个阶段的目标是完成所有业务的检查,并锁定必要的资源,为后续的确认提交做准备。
1. 订单服务 - tryCreateOrder
- 操作:检查订单合法性(如商品是否存在、价格是否正确),然后将订单状态设置为 “预创建”,并锁定对应商品的库存。此时订单对用户不可见。
- 资源预留:商品库存被锁定,防止超卖。
2. 支付服务 - tryPayment
- 操作:检查用户账户余额是否充足。如果充足,则冻结支付所需的金额(例如,订单100元,则从可用余额中扣除100元,放入冻结金额字段)。
- 资源预留:用户的100元资金被冻结,不能再用于其他支付。
3. 积分服务 - tryAddPoints
- 操作:检查用户是否存在,然后准备一笔 “待增加” 的积分记录,状态为 **”预增加”**,并不实际改变用户的积分总额。
- 资源预留:标记了一笔积分即将到账。
至此,Try阶段结束。 所有必要的业务检查都已通过,且资源都已预留。如果任何一个服务的Try操作失败(如库存不足、余额不足),则全局事务将进入Cancel阶段。
第二阶段:Confirm 阶段(业务确认)
此阶段在Try阶段全部成功之后执行。因为所有资源都已预留,所以Confirm操作必须成功,且需要是幂等的。
1. 订单服务 - confirmCreateOrder
- 操作:将订单状态从 “预创建” 更新为 **”已创建”**,使订单对用户可见。
- 特点:这是一个轻量的更新操作,因为所有检查在Try阶段已完成。
2. 支付服务 - confirmPayment
- 操作:将之前冻结的金额从冻结字段中正式扣除。用户的账户余额实际减少。
- 特点:真正的扣款发生在此刻。
3. 积分服务 - confirmAddPoints
- 操作:将积分记录的状态从 “预增加” 更新为 “已增加”,并实际增加用户的积分总额。
- 特点:用户真正收到积分。
至此,Confirm阶段成功,整个分布式事务完成。
补偿阶段:Cancel 阶段(回滚操作)
如果Try阶段有任何一步失败,或者事务管理器决定回滚,则会进入Cancel阶段。Cancel操作需要执行与Try操作相反的逻辑,释放Try阶段预留的资源,并且也必须是幂等的。
假设在tryPayment时发现用户余额不足:
1. 积分服务 - cancelAddPoints
- 操作:删除或作废那条状态为 “预增加” 的积分记录。
2. 支付服务 - cancelPayment
- 操作:无需操作,因为
tryPayment已经失败,没有资金被冻结。或者,如果Try是部分成功(如冻结了资金但后续服务失败),则此操作需要解冻冻结的资金。
3. 订单服务 - cancelCreateOrder
- 操作:将订单状态设置为 “已取消”,并释放在Try阶段锁定的商品库存。
至此,Cancel阶段完成,所有服务都回到了事务开始前的状态。
流程总览与关键点
| 阶段 | 订单服务 | 支付服务 | 积分服务 | 说明 |
|---|---|---|---|---|
| Try | 置订单为”预创建”,锁库存 | 检查并冻结资金 | 准备积分,状态为”预增加” | 资源预留,任一失败则触发Cancel |
| Confirm | 置订单为”已创建” | 正式扣款 | 正式增加积分 | 业务确认,必须幂等 |
| Cancel | 置订单为”已取消”,释库存 | 解冻资金 | 作废积分记录 | 补偿回滚,必须幂等 |
核心要求与挑战
- 幂等性:Confirm和Cancel接口可能会因为网络问题被重复调用,必须保证多次调用的结果与一次调用相同。
- 空回滚:如果Try阶段没执行(如网络超时),但Cancel请求却来了,需要能够处理并返回成功。
- 防悬挂:如果Cancel请求先于Try请求到达(网络包乱序),要能识别并忽略后续来的Try请求。
- 业务侵入性高:需要将业务逻辑清晰地拆分为Try/Confirm/Cancel三个部分,设计和开发复杂度高。
总结:TCC通过业务层面的补偿机制,避免了像2PC那样长时间的资源锁定,提供了更高的性能和解耦能力,但代价是需要为每个服务设计并实现完整的补偿逻辑,对业务设计和开发的要求非常高。
本地消息表
这是一种最终一致性的解决方案,其核心思想是利用本地数据库的事务性,将消息生产和业务操作绑定在同一个本地事务中,然后通过异步重试机制来保证消息一定能被下游服务消费。
核心流程
- 消息生产与业务操作同步
- 业务执行方在同一个本地数据库事务中:
- 执行业务SQL操作(如:订单库中插入一条订单记录,状态为
待确认)。 - 向同一数据库的本地消息表插入一条消息记录(内容为“扣减库存”或“增加积分”等),此消息的状态为
进行中或未发送。
- 执行业务SQL操作(如:订单库中插入一条订单记录,状态为
- 业务执行方在同一个本地数据库事务中:
- 异步投递消息
- 一个独立的消息发送者(通常是定时任务)轮询扫描本地消息表,抓取状态为
进行中的消息。 - 将消息内容发送给消息队列。
- 如果发送成功,就将本地消息表中原记录的状态更新为
已发送或直接删除该记录。
- 一个独立的消息发送者(通常是定时任务)轮询扫描本地消息表,抓取状态为
- 消息消费与确认
- 下游服务(如库存服务)消费消息队列中的消息,执行自身的业务逻辑。
- 执行成功后,下游服务可以选择向另一个回调队列发送一条“成功确认”消息。
- 补偿机制(扫表重试)
- 这是该方案的关键。另一个定时任务会周期性地检查本地消息表中长时间处于
进行中状态的消息。 - 对于这些消息,它会重新执行第2步(投递消息)。由于消息可能已经被成功消费但确认失败,所以下游服务的接口必须支持幂等,防止重复处理。
- 这是该方案的关键。另一个定时任务会周期性地检查本地消息表中长时间处于
优缺点
- 优点:
- 方案简单,与具体MQ组件解耦,甚至可以用MySQL表代替MQ。
- 实现了业务的最终一致性。
- 缺点:
- 消息数据与业务数据强耦合,占用业务数据库资源。
- 需要维护额外的定时任务来扫表和重发,架构稍显繁琐。
- 实时性相对较差,依赖于定时任务的扫描间隔。
基于事务消息
这种方案通常需要支持事务消息模型的中间件(如RocketMQ)来原生支持。其核心思想是通过两阶段提交,将消息的投递与本地事务绑定,确保消息能成功发出,并且最终一定能被消费。
核心流程(以RocketMQ为例)
- 发送半消息
- 业务执行方首先向MQ Server发送一条 “半消息” 或 “预备消息”。这条消息对下游消费者是不可见的,即此时不会被消费。
- 执行本地事务
- MQ Server持久化半消息后,返回确认。
- 业务执行方开始执行本地事务(如:在订单库中插入订单记录)。
- 提交或回滚消息
- 本地事务执行完成后,根据结果向MQ Server发送 二次确认:
- Commit: 本地事务成功。MQ Server此时将半消息转为正式消息,下游消费者现在可以消费这条消息了。
- Rollback: 本地事务失败。MQ Server将丢弃这条半消息。
- 未知: 如果因为网络等原因,MQ Server没有收到二次确认。
- 本地事务执行完成后,根据结果向MQ Server发送 二次确认:
- 回查机制
- 对于状态为 未知 的消息,MQ Server会定期向消息生产者发起 回查。
- 生产者需要检查对应的本地事务的执行状态。
- 根据检查结果(成功/失败),再次向MQ Server发送Commit或Rollback指令。
- 消息消费
- 一旦消息被Commit,下游服务即可正常消费。同样,下游服务接口需要幂等处理,因为网络抖动可能导致消息重投。
优缺点
- 优点:
- 数据隔离性好,消息逻辑不侵入业务数据库。
- 由MQ中间件提供高可用的消息存储和可靠投递,可靠性更高。
- 无需自己实现扫表任务,架构更优雅。
- 缺点:
- 强依赖于支持事务消息的MQ,技术选型受限。
- 需要实现回查接口,增加了开发工作量。
在XA协议中,对比二阶段,三阶段有什么改进
二阶段:

三阶段:

第一阶段(prepare):每个参与者执行本地事务但不提交,进入 ready 状态,并通知协调者已经准备就绪。
第二阶段(commit)当协调者确认每个参与者都 ready 后,通知参与者进行 commit 操作;如果有参与者 fail ,则发送 rollback 命令,各参与者做回滚。
问题:
- 单点故障:一旦事务管理器出现故障,整个系统不可用(参与者都会阻塞住)
- 数据不一致:在阶段二,如果事务管理器只发送了部分 commit 消息,此时网络发生异常,那么只有部分参与者接收到 commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
- 响应时间较长:参与者和协调者资源都被锁住,提交或者回滚之后才能释放
- 不确定性:当协调者管理器发送 commit 之后,并且此时只有一个参与者收到了 commit,那么当该参与者与事务管理器同时机之后,重新选举的事务管理器无法确定该条消息是否提交成功。
三阶段协议:主要是针对两阶段的优化,解决了2PC单点故障的问题,但是性能问题和不一致问题仍然没有根本解决
2PC 是 两阶段提交 的缩写,英文全称为 Two-Phase Commit
引入了超时机制解决参与者阻塞的问题,超时后本地提交,2pc只有协调者有超时机制
第一阶段:
CanCommit阶段,协调者询问事务参与者,是否有能力完成此次事务。如果都返回
yes,则进入第二阶段有一个返回
no或等待响应超时,则中断事务,并向所有参与者发送abort请求第二阶段:
PreCommit阶段,此时协调者会向所有的参与者发送PreCommit请求,参与者收到后开始执行事务操作。参与者执行完事务操作后(此时属于未提交事务的状态),就会向协调者反馈“Ack”表示我已经准备好提交了,并等待协调者的下一步指令。第三阶段:
DoCommit阶段,在阶段二中如果所有的参与者节点都返回了Ack,那么协调者就会从“预提交状态”转变为“提交状态”。然后向所有的参与者节点发送“doCommit”请求,参与者节点在收到提交请求后就会各自执行事务提交操作,并向协调者节点反馈“Ack”消息,协调者收到所有参与者的Ack消息后完成事务。相反,如果有一个参与者节点未完成PreCommit的反馈或者反馈超时,那么协调者都会向所有的参与者节点发送abort请求,从而中断事务。
如何理解RPC
远程过程调用
RPC要求在调用方中放置被调用的方法的接口。调用方只要调用了这些接口,就相当于调用了被调用的实际方法,十分易用。于是,调用方可以像调用内部接口一样调用远程的方法,而不用封装参数名和参数值等操作。
包含组件
- 动态代理:封装调用细节
- 序列化与反序列化:数据传输与接收
- 通信:可以选择七层的HTTP,四层的TCP/UDP
- 异常处理等
RPC调用流程
调用方流程:
首先,调用方调用的是接口,必须为接口构造一个假的实现。显然,要使用动态代理。这样,调用方的调用就被动态代理接收到了。
动态代理收到调用后,应该想办法调用远程的实际实现。这包括下面几步:
- 识别具体要调用的远程方法的IP、端口
- 将调用方法的入参进行序列化
- 通过通信将请求发送到远程的方法中
被调用方流程:
这样,远程的服务就接收到了调用方的请求。它应该:
- 反序列化各个调用参数
- 定位到实际要调用的方法,然后输入参数,执行方法
- 按照调用的路径返回调用的结果




