前言
随着微服务架构的普及,一个业务操作往往需要跨越多个数据库、多个服务节点,传统的单机数据库事务(ACID)已经无法满足这种分布式场景下的数据一致性要求。
分布式事务,成为了每一个后端开发者必须面对的挑战。
本文将从理论基础出发,系统讲解6种主流的分布式事务解决方案,包括它们的实现原理、优缺点、适用场景,并在最后给出清晰的选型建议。无论你是刚接触分布式事务,还是已经在项目中遇到一致性问题,相信本文都能给你带来帮助。
一、分布式事务产生的背景
在单体应用中,我们通常利用数据库自身的ACID特性来保证事务的原子性和一致性。例如,在一个方法上加上@Transactional注解,多个SQL操作要么全部成功,要么全部回滚。
然而,在微服务架构下,情况发生了根本变化:
-
数据被拆分到不同的数据库(分库分表)
-
业务逻辑分布在不同的服务节点上
-
一次用户请求需要调用多个远程服务
在这种情况下,原本的本地事务无法跨数据库、跨服务生效,分布式事务的问题由此产生。
一个典型的例子:电商下单场景。
用户下单操作通常需要同时涉及:
-
订单服务:创建订单记录
-
库存服务:扣减商品库存
-
账户服务:扣减用户余额
这三个操作可能分别操作不同的数据库,甚至运行在不同的物理节点上。如何保证三者要么同时成功,要么同时失败?这就是分布式事务要解决的核心问题。
二、理论基础:CAP与BASE
在正式介绍解决方案之前,有必要先理解两个基础理论:CAP定理和BASE理论。它们是所有分布式事务方案的底层指导思想。
2.1 CAP定理
CAP定理由Eric Brewer教授在2000年提出,它指出一个分布式系统不可能同时满足以下三个特性:
| 特性 | 英文 | 解释 |
|---|---|---|
| 一致性 | Consistency | 所有节点在同一时刻看到相同的数据 |
| 可用性 | Availability | 每个请求都能收到成功或失败的响应 |
| 分区容错性 | Partition Tolerance | 系统在网络分区(节点间通信中断)时仍能正常工作 |
由于网络分区在实际的分布式系统中是不可避免的(网络总有可能出现故障),因此P(分区容错性)是必须保证的。这样一来,我们实际上只能在C(一致性)和A(可用性)之间做权衡:
-
CP系统:保证一致性和分区容错性,牺牲可用性。当网络分区发生时,系统会暂停服务,等待数据达成一致。
-
AP系统:保证可用性和分区容错性,牺牲一致性。当网络分区发生时,系统继续提供服务,但数据可能会出现短暂的不一致,最终再达成一致。
2.2 BASE理论
BASE理论是对CAP定理中一致性权衡结果的进一步延伸,它指出一个大规模分布式系统很难实现严格的强一致性,但可以实现最终一致性。BASE由以下三个要素组成:
-
BA(Basically Available,基本可用):系统保证核心可用,允许部分功能降级。比如大促期间,下单服务正常,但查看历史订单可能延迟。
-
S(Soft State,软状态):允许系统中存在中间状态,并且认为这个中间状态不影响系统的整体可用性。比如"订单支付中"这个状态,数据虽然没最终确定,但系统仍可接受新的请求。
-
E(Eventually Consistent,最终一致性):经过一段时间后,所有数据副本最终会达到一致状态。
理解这两个理论很重要,因为绝大多数分布式事务方案追求的都是最终一致性,而非强一致性。如果你的业务场景要求强一致性(比如银行转账),那么就需要接受性能上的损失。
三、六大分布式事务解决方案
下面按照从强一致到最终一致的顺序,详细讲解6种解决方案。
3.1 两阶段提交(2PC)
两阶段提交是最经典的分布式事务解决方案,它被广泛实现在各种关系型数据库中,比如MySQL的XA事务。
3.1.1 角色组成
2PC协议中涉及两个角色:
-
协调者(Coordinator):负责整个事务的调度和决策,通常由事务管理器充当。
-
参与者(Participant):实际执行操作的资源管理器,比如数据库。
3.1.2 执行流程
第一阶段:准备阶段(Prepare Phase)
-
协调者向所有参与者发送"准备"请求,询问是否能够提交事务。
-
每个参与者执行事务操作,但不提交(此时数据被锁定),然后向协调者返回"可以提交"或"不能提交"的投票结果。
第二阶段:提交阶段(Commit Phase)
-
如果所有参与者都返回"可以提交",协调者向所有参与者发送"提交"指令,参与者正式提交事务,释放锁资源。
-
如果有任何一个参与者返回"不能提交"或者超时未响应,协调者向所有参与者发送"回滚"指令,参与者撤销之前的操作。
3.1.3 优点与缺点
优点:
-
实现原理相对简单,易于理解。
-
能够保证强一致性,所有参与者要么全部提交,要么全部回滚。
-
被主流关系型数据库支持,技术成熟。
缺点:
-
同步阻塞问题:参与者从准备阶段开始就会锁定资源,直到提交或回滚完成。在高并发场景下,这会严重影响系统吞吐量。
-
协调者单点故障:如果协调者在第二阶段宕机,所有参与者会长时间锁定资源而无法释放。
-
数据不一致风险:在第二阶段,如果协调者发送"提交"后部分参与者发生故障,就会出现部分提交、部分未提交的不一致情况。
3.1.4 适用场景
适合对数据一致性要求极高、并发量较低、事务执行时间短的场景,例如银行核心系统的转账交易。
3.2 三阶段提交(3PC)
三阶段提交是对2PC的改进,旨在解决2PC中的协调者单点阻塞问题。
3.2.1 主要改进
3PC相比2PC增加了以下改进:
-
增加CanCommit阶段:在真正的准备阶段之前,先询问所有参与者是否存活且就绪。这样可以在早期发现不可用的节点。
-
引入超时机制:参与者在等待协调者指令时,如果超时未收到响应,会自动执行默认操作(通常是提交)。
3.2.2 执行流程
第一阶段:CanCommit
协调者询问所有参与者是否可以进行事务操作。参与者只检查自身状态(如网络、资源),不执行实际业务操作。
第二阶段:PreCommit
如果所有参与者都同意,协调者发送PreCommit请求,参与者开始执行事务操作(仍然不提交),并返回准备就绪信号。
第三阶段:DoCommit
如果所有参与者都准备就绪,协调者发送DoCommit请求,参与者正式提交事务。
3.2.3 优点与缺点
优点:
-
降低了协调者宕机导致的阻塞概率。如果参与者在等待过程中超时,会自动提交,而不是一直阻塞。
-
多了一次预检查,可以在早期发现节点故障。
缺点:
-
并没有完全解决数据不一致的问题。在网络分区的情况下,部分参与者自动提交可能导致数据不一致。
-
实现复杂度高,性能仍然不够理想。
-
实际工业界应用很少,更多的是理论价值。
3.2.4 适用场景
了解其原理即可,实际项目中很少单独使用3PC。
3.3 TCC(Try-Confirm-Cancel)
TCC是一种基于业务层面的分布式事务解决方案,它不依赖数据库的XA协议,而是由业务代码手动控制事务的每个阶段。
3.3.1 核心概念
TCC将一个完整的业务操作拆分成三个步骤:
-
Try(尝试):预留业务资源,做业务检查和准备工作。比如扣减库存时,先冻结库存而不是直接扣减。
-
Confirm(确认):真正执行业务操作,使用Try阶段预留的资源。Confirm操作必须保证幂等。
-
Cancel(取消):取消Try阶段预留的资源,释放冻结的资源。Cancel操作也必须保证幂等。
3.3.2 执行流程
以电商下单扣库存为例:
-
Try阶段:
-
订单服务:创建状态为"待确认"的订单记录。
-
库存服务:冻结用户要购买的库存数量(将可用库存转为冻结库存)。
-
账户服务:冻结用户要支付的金额。
-
-
Confirm阶段:
-
订单服务:将订单状态更新为"已完成"。
-
库存服务:将冻结库存真正扣除,减少实际库存。
-
账户服务:将冻结金额真正扣除,减少账户余额。
-
-
Cancel阶段(如果任何一个Confirm失败或业务取消):
-
订单服务:将订单状态更新为"已取消"。
-
库存服务:释放冻结库存,恢复到可用库存。
-
账户服务:释放冻结金额,恢复到账户余额。
-
3.3.3 空回滚与幂等问题
在使用TCC时,需要特别注意两个问题:
-
空回滚:Cancel阶段执行时,对应的Try阶段可能从未执行过(比如Try阶段请求超时但后来到达)。Cancel操作要能正确处理这种情况,不能因为找不到Try记录而失败。
-
幂等性:Confirm和Cancel可能会因为网络重试被多次调用,业务实现必须保证多次调用的结果与一次调用相同。
3.3.4 优点与缺点
优点:
-
高性能:整个过程中没有长期的资源锁定,并发能力远高于2PC。
-
最终一致性:通过Confirm和Cancel机制保证数据最终一致。
-
灵活性高:开发者可以根据业务特点精细控制事务粒度。
缺点:
-
业务侵入大:需要为每个业务操作实现Try、Confirm、Cancel三个接口,代码量大幅增加。
-
开发复杂度高:需要仔细处理幂等、空回滚、悬挂等问题。
-
对业务设计有要求:并非所有业务都能很好地抽象出Try阶段的可预留资源。
3.3.5 适用场景
适合对性能要求高、业务本身具备可预留资源特征的场景,典型的包括:
-
支付系统:冻结资金
-
库存系统:冻结库存
-
票务系统:锁定座位
3.4 本地消息表
本地消息表是分布式事务的一种经典实现方案,最早由eBay提出。它的核心思想是将分布式事务拆解为多个本地事务 + 异步消息。
3.4.1 核心架构
本地消息表方案包含以下核心组件:
-
消息表:在业务数据库(本地数据库)中创建一张消息表,用于记录待发送的消息。
-
定时任务:定期扫描消息表,将未发送的消息投递到MQ或直接调用目标服务。
-
消息消费方:处理消息,并保证幂等性。
3.4.2 执行流程
-
业务操作和消息插入在同一个本地事务中完成。例如:
-
执行本地业务SQL(如更新订单状态)
-
向本地消息表插入一条"待发送"的消息记录
-
提交本地事务
-
-
独立的定时任务(或后台线程)扫描消息表中的未发送消息。
-
对于每条待发送消息,定时任务将其发送到消息队列(或直接调用下游服务)。
-
消息消费方收到消息后,执行对应的业务操作。
-
消费方处理成功后,通知消息表发送方删除或标记该消息为"已处理"。
-
如果消费方处理失败,消息会保留在消息队列中,等待重试。
3.4.3 优点与缺点
优点:
-
实现相对简单,不依赖特定的MQ产品。
-
可靠性较高,消息存储在本地数据库中,不会丢失。
-
适合对消息可靠性要求高、对实时性要求不高的场景。
缺点:
-
效率较低:定时轮询扫描消息表,存在延迟,且对数据库造成额外压力。
-
耦合性高:消息表和业务表在同一个数据库中,增加了数据库的负担。
-
扩展性差:随着业务量增长,单表消息量过大会影响性能。
3.4.4 适用场景
适合中小型项目,或者对MQ依赖不强、希望使用简单方案实现最终一致性的场景。早期的电商系统大量使用这种方案实现订单状态同步。
3.5 事务消息(RocketMQ)
事务消息是RocketMQ提供的一种原生支持分布式事务的特性,它本质上是对"本地消息表"思想的优化版本,将消息的存储和状态管理从业务数据库移到了MQ内部。
3.5.1 核心概念
RocketMQ的事务消息引入了两个重要概念:
-
半消息(Half Message):一种特殊消息,它已经被MQ接收并存储,但暂不可被消费者拉取。只有当本地事务执行成功并提交后,半消息才会被标记为可消费。
-
消息回查(Message Status Check):MQ会定期向发送方询问那些长时间处于"半消息"状态的消息,确认本地事务的实际结果。
3.5.2 执行流程
-
发送半消息:事务发起方向RocketMQ发送一条半消息(Half Message),此时消息已存储在MQ中,但消费者还不可见。
-
执行本地事务:发起方执行本地业务操作(如更新数据库)。
-
提交或回滚半消息:
-
如果本地事务执行成功,向MQ发送确认指令,将半消息提交,此时消息对消费者可见。
-
如果本地事务执行失败,向MQ发送回滚指令,删除半消息。
-
-
消息回查(补偿机制):
-
如果发起方在第3步执行后宕机,或者没有及时发送确认指令,MQ会定期向发起方发起回查请求。
-
发起方根据本地事务的状态,返回"提交"或"回滚"。
-
-
消息消费:消费者拉取到已提交的消息,执行对应的业务操作。消费者需要保证幂等性。
3.5.3 优点与缺点
优点:
-
高性能:相比本地消息表,减少了数据库轮询,吞吐量更高。
-
解耦:业务服务和MQ之间解耦,无需在业务数据库中维护消息表。
-
可靠性好:RocketMQ本身支持高可用,消息不丢失。
缺点:
-
依赖特定MQ:只有RocketMQ支持事务消息(Kafka和RabbitMQ原生不支持)。
-
消费者需幂等:消息可能重复投递,消费端必须做好幂等处理。
-
实现相对复杂:需要实现回查接口,理解半消息的概念。
3.5.4 适用场景
适合对吞吐量有较高要求、业务允许最终一致性的异步场景。典型应用包括:
-
订单完成后发送积分
-
支付成功后发送短信通知
-
用户注册后发送欢迎邮件
3.6 Seata AT模式(自动补偿)
Seata是阿里巴巴开源的一款分布式事务解决方案,其中的AT模式是其核心特性。AT模式最大的特点是对业务代码几乎无侵入,开发者就像写本地事务一样写分布式事务。
3.6.1 核心原理
Seata AT模式的工作原理可以概括为:拦截SQL + 生成回滚日志 + 全局锁。
整体架构包含三个核心组件:
-
TC(Transaction Coordinator):事务协调器,独立部署的Seata服务端,负责维护全局事务的状态。
-
TM(Transaction Manager):事务管理器,嵌入在业务应用中的Seata客户端,负责开启、提交、回滚全局事务。
-
RM(Resource Manager):资源管理器,同样嵌入在业务应用中,负责管理分支事务的资源(数据库连接)和SQL执行。
3.6.2 执行流程
-
开启全局事务:TM向TC申请一个全局事务ID(XID)。
-
执行业务SQL:
-
业务代码中正常执行SQL(如
update product set stock = stock - 1 where id = 1)。 -
Seata的RM拦截到这条SQL,会执行以下操作:
-
生成SQL执行前后的数据快照(
before image和after image)。 -
将快照数据写入
undo_log表。 -
执行真正的业务SQL。
-
-
-
获取全局锁:在提交本地事务前,RM会向TC获取全局锁。只有成功获取全局锁,才能提交本地事务;否则会重试或回滚。
-
提交本地事务:提交业务数据库的本地事务,同时提交
undo_log记录。 -
提交全局事务:所有分支事务都完成后,TM通知TC提交全局事务。TC会释放全局锁,并异步删除
undo_log记录。 -
回滚全局事务:如果任意一个分支事务失败,TM通知TC回滚。TC会通知所有RM进行回滚。RM根据
undo_log中的前置快照,生成回滚SQL并执行,将数据恢复到业务SQL执行前的状态。
3.6.3 优点与缺点
优点:
-
对业务代码无侵入:只需要在入口方法上添加
@GlobalTransactional注解,不需要像TCC那样手动实现三个接口。 -
自动生成回滚SQL:框架自动完成,无需手动编写补偿逻辑。
-
全局锁机制:保证不同全局事务之间的数据隔离性。
缺点:
-
性能低于TCC:因为需要写
undo_log、获取全局锁等额外操作。 -
复杂SQL支持有限:对于存储过程、复杂的多表关联更新,快照生成可能不准确。
-
需要独立部署TC:多了一个运维组件。
3.6.4 适用场景
适合希望快速接入分布式事务、不想对业务代码进行大量改造的微服务项目。很多从单机迁移到分布式的项目,Seata AT是最平滑的选择。
四、方案对比与选型建议
4.1 整体对比
| 方案 | 一致性类型 | 性能 | 代码侵入 | 运维成本 | 技术依赖 |
|---|---|---|---|---|---|
| 2PC | 强一致性 | 低 | 低 | 低 | 数据库支持XA |
| TCC | 最终一致性 | 高 | 高 | 中 | 无 |
| 本地消息表 | 最终一致性 | 中 | 中 | 低 | 数据库 + MQ |
| 事务消息 | 最终一致性 | 高 | 中 | 中 | RocketMQ |
| Seata AT | 强一致性(全局锁) | 中 | 极低 | 中 | Seata服务 |
4.2 选型决策树
可以根据以下问题快速定位适合你的方案:
text
问1:你的业务是否要求强一致性(不允许任何中间状态)? ├─ 是 → 问2 └─ 否 → 跳转到问3 问2:你的系统并发量高吗? ├─ 是 → Seata AT(全局锁保证强一致,性能尚可) └─ 否 → 2PC(传统但可靠) 问3:你能接受对业务代码进行大规模改造吗? ├─ 是 → TCC(最高性能,但开发成本高) └─ 否 → 问4 问4:你的技术栈中是否已经使用了RocketMQ? ├─ 是 → 事务消息 └─ 否 → 问5 问5:你的系统规模大吗,未来会持续增长吗? ├─ 是 → Seata AT(可扩展性好,社区活跃) └─ 否 → 本地消息表(简单可靠,适合中小项目)
4.3 几条实用建议
-
不要过早引入分布式事务。先审视业务,看是否可以通过设计避免分布式事务,比如调整业务流程、数据冗余等。
-
优先考虑最终一致性。大多数业务场景(如订单、通知、积分)都能接受短暂的不一致,最终一致即可。
-
TCC虽然强大,但要谨慎使用。它的实现成本很高,只有在真正需要高性能且业务适合预留资源的场景下才考虑。
-
Seata AT是当前比较平衡的选择。如果你正在选型且没有强烈的偏好,可以从Seata AT开始,它既能保证一定的一致性,又对代码侵入小。
五、写在最后
分布式事务是一个"没有银弹"的领域,每种方案都有自己的适用边界。作为架构师或开发者,我们需要做的是:
-
理解每个方案的原理和局限
-
明确自己的业务对一致性、性能、开发成本的要求
-
在多个维度之间做出合理的权衡
希望本文能帮助你在实际项目中做出正确的选型。如果你在实践中有任何疑问或经验,欢迎在评论区留言交流。
如果本文对你有帮助,欢迎点赞、收藏、转发,让更多人看到。
参考资料:
-
《分布式一致性原理与实践》
-
Seata官方文档:https://seata.io/zh-cn/
-
RocketMQ事务消息官方文档
2万+

被折叠的 条评论
为什么被折叠?



