# 分布式事务解决方案

# 分布式事务
在分布式系统中,由于服务和数据库被拆分到不同的节点上,传统的本地事务(ACID)无法直接适用。为了保证数据的一致性,业界演进出了多种分布式事务解决方案。
这些方案通常可以分为两大阵营:追求强一致性的刚性事务,和追求最终一致性的柔性事务(基于 BASE 理论)
# 强一致性方案(刚性事务)
这类方案要求所有节点在事务提交时,数据必须完全一致,适用于对数据准确性要求极高的场景(如金融转账)
# 2PC(Two-Phase Commit,二阶段提交)
将事务的提交过程分为两个阶段:准备阶段和提交阶段,由一个“协调者”来统一指挥多个“参与者”
P1 - 准备阶段 (Prepare): 协调者向所有参与者发送准备请求,参与者执行本地事务但不提交,并锁定资源,然后向协调者回复 Yes 或 No
P2 - 提交阶段 (Commit): 如果所有参与者都回复 Yes,协调者下发 Commit 指令;只要有一个回复 No 或超时,协调者下发 Rollback 指令
缺点:
- 同步阻塞:参与者在等待协调者指令时,会一直锁定资源,性能极差
- 单点故障: 如果协调者在第二阶段挂了,参与者会陷入无休止的等待
- 数据不一致: 如果在 Commit 阶段发生网络分区,部分参与者收到了指令而部分没收到,会导致数据不一致
# 3PC(Three-Phase Commit,三阶段提交)
为了解决 2PC 的同步阻塞和单点故障问题,3PC 在 2PC 的基础上引入了超时机制,并将原本的准备阶段一拆为二,演进为三个阶段
P1 - 询问阶段 (CanCommit): 协调者向参与者发送询问请求。参与者只做基本的自身状态和资源检查,口头回复 Yes 或 No,此时不锁定资源
P2 - 预提交阶段 (PreCommit): 如果 P1 全部回复 Yes,协调者下发预提交请求。参与者此时执行本地事务并锁定资源,但不提交,随后向协调者回复 Ack
P3 - 提交阶段 (DoCommit): 如果 P2 全部回复 Ack,协调者下发正式 Commit 指令,参与者正式提交事务并释放资源
改善点: 在 P1(CanCommit)阶段只是口头询问而不锁资源。在 P3 阶段,如果参与者长时间收不到协调者的 Commit 指令,参与者超时后会自动提交事务,避免了 2PC 中无限期等待的单点故障
数据不一致问题依然存在: 如果在 P3 阶段发生网络分区,协调者实际想下发 Rollback(回滚),但部分参与者由于网络断开没收到,超时后它们自动提交了,而收到指令的执行了回滚,导致数据严重不一致。因此工业界极少使用
# 最终一致性方案(柔性事务)
这类方案核心思想是:允许中间状态的不一致,只要保证最终数据一致即可。性能通常远高于 2PC
# TCC
TCC 是一种业务层面的二阶段提交,需要侵入业务代码,手动实现三个接口。TCC这种方案应该是在企业中应用比较广泛的一种方案。TCC是Try、Confirm、Cancel三个词语的缩写
TCC主要分为3个操作
Try:一阶段,负责业务资源检查和预留
Confirm:二阶段提交操作,所有的Try都成功了,则执行Confirm操作。Confirm真正执行业务,使用Try预留的资源
Cancel:二阶段回滚操作,只要有一个Try失败了,则走到Cancel操作。Cancel释放Try预留的资源

优点:并发程度高,在业务层面锁定资源
缺点:业务侵入性强,一个业务需要提供Try/Confirm/Cancel三个方法
# SAGA
Saga 是一种长事务解决方案。它将一个分布式事务拆分为若干个本地局部事务
正常流程: 顺次执行
异常流程: 如果在执行到
- 正向恢复(Forward Recovery): 重试失败的
,适用于必须要成功的场景 - 反向恢复(Backward Recovery): 顺次执行补偿事务,即
,冲正前面已经提交的事务

SAGA特点为
- 并发度高,不需要长期锁定资源
- 开发量大,需要定义正向操作和补偿操作
- 不能保证隔离型
# 本地消息表
这是大型互联网公司非常经典的模式,利用了数据库本地事务和消息队列
- A 系统在执行本地业务时,顺便在同一个数据库的“本地消息表”里插入一条消息,利用本地事务保证业务和消息同时成功。
- A 系统有一个后台任务,轮询本地消息表,把消息发到 MQ。
- B 系统消费 MQ 消息,执行自己的业务。执行成功后,通知 A 系统把消息状态改为“已完成”。
- 如果 B 失败或超时,A 的后台任务会不断重试发送消息,直到 B 成功(B 需做幂等处理)
通过这种方案就能达到事务的最终一致性,这种不断重试的思路,也体现了我们后面要提到的最大努力通知
本地消息表特点为:
- 需要创建额外的消息表,不断对消息表轮询
# RocketMQ事务消息
在本地消息表方案中,生产者需要额外创建本地消息表,还要对本地消息进行轮询。RocketMQ在4.3之后的版本正式支持事务消息,该事务消息的本质是把本地消息表放在RocketMQ上,解决生产端消息发送和本地事务执行的原子性问题
RocketMQ实现分布式事务的流程如下
- producer向mq server发送一个半消息
- mq server将消息持久化成功后,向发送方确认消息已经发送成功,此时消息并不会被consumer消费
- producer开始执行本地事务逻辑
- producer根据本地事务执行结果向mq server发送二次确认,mq收到commit状态,将消息标记为可投递,consumer会消费该消息。mq收到rollback则删除半消息,consumer将不会消费该消息,如果收到unknow状态,mq会对消息发起回查
- 在断网或者应用重启等特殊情况下,步骤4提交的2次确认有可能没有到达mq server,经过固定时间后mq会对该消息发起回查
- producer收到回查后,需要检查本地事务的执行状态
- producer根据本地事务的最终状态,再次提交二次确认,mq仍按照步骤4对半消息进行操作
看到这,可能有人会问了,我们先执行本地事务,执行成功后再发送消息,这样不也可以保证生产端消息发送和本地事务执行的原子性?
其实这样做还是有可能会造成数据不一致的问题。假如本地事务执行成功,发送消息,由于网络延迟,消息发送成功,但是回复超时了,抛出异常,本地事务回滚。但是消息其实投递成功并被消费了,此时就会造成数据不一致的情况
那消息投递到mq server,consumer消费失败怎么办?
如果是消费超时,重试即可。如果是由于代码等原因真的消费失败了,此时就得人工介入,重新手动发送消息,达到最终一致性。
# 最大努力通知
做过微信充值或者支付宝充值的小伙伴对这个方案应该比较熟悉,因为最大努力通知这种方案在充值系统中经常被使用
充值系统通过不断的重试将充值结果推送给账户系统。因此账户系统接收充值结果的系统要保持幂等。另外充值充值系统还要提供回查接口,让账户系统主动校验充值的状态

# Seata AT模式
它在底层通过自动生成逆向 SQL(回滚日志)的方式,实现了类似 2PC 的效果,但对业务完全无侵入
一阶段: Seata 的代理数据源会拦截你的 SQL,在业务数据修改前保存“前镜像”到undo_log表,业务修改后保存“后镜像”到undo_log表,然后提交本地事务
二阶段:
- 如果全局事务成功,异步快速删除 undo_log
- 如果全局事务失败,读取对应的 undo_log,进行脏写校验(对比当前数据库真实数据与“后镜像”,如果不一致说明数据被别人动过,需要人工介入),如果校验通过,利用 undo_log 的前镜像数据,自动反向生成 SQL 回滚数据
Seata AT模式特点:
- 对代码无侵入,开发速度较快
- 需要用全局锁来保证隔离性,并发程度较低
# 总结
| 方案 | 一致性级别 | 性能 | 开发成本 | 适用场景 |
|---|---|---|---|---|
| 2PC / 3PC | 强一致性 | 极低 | 低 (框架层) | 跨库的微小事务,对并发要求不高的传统行业(如 ERP、传统 OA) |
| TCC | 最终一致性 | 高 | 极高 | 核心金融、转账、账务系统等对资金安全要求极高的核心业务 |
| Saga | 最终一致性 | 高 | 中 | 业务流程长、参与者多、或者需要调用第三方服务的场景 |
| 本地消息表 / RocketMQ事务消息 | 最终一致性 | 高 | 中 | 跨服务异步解耦、对实时性要求不高的外围业务(如发券、积分、通知) |
| Seata AT | 最终一致性 | 中高 | 极低 | 并发量不高、追求快速开发、不想改动业务代码的微服务系统(企业中后台) |
← 如何对微服务进行拆分? 注册中心选型 →