一、什么是分布式事务?
在现代微服务架构下,业务逻辑通常被拆分到多个独立的服务中。这带来了一个经典难题:当一个业务操作需要跨多个服务更新数据时,如何确保这些操作要么全部成功,要么全部失败?这就是分布式事务要解决的核心问题。
简单来说,分布式事务是指事务的参与者、资源服务器以及事务管理器分别位于不同的分布式节点之上。我们需要保证这些跨节点的操作具有原子性,从而维护全局数据的一致性。
一个典型的场景是电商下单:
- 订单服务:创建订单记录。
- 库存服务:扣减商品库存。
- 账户服务:从用户账户扣款。
这三个操作分属三个不同的服务和数据库,但它们必须作为一个整体(原子操作)来完成。如果创建订单成功,但扣减库存失败,整个交易就应该回滚,否则会导致数据不一致(有订单但没减库存)。

实现分布式事务,主要面临三大挑战:
- 网络不确定性:服务间通信可能发生延迟、超时甚至彻底失败,如何判断对方的状态?
- 节点故障:任意一个服务节点宕机,都可能导致整个事务“卡”在半路,状态不一致。
- 性能瓶颈:跨网络的多次协调和资源锁定,必然会引入更高的延迟,影响系统吞吐量。
为了解决这些挑战,业界发展出了两大思路:刚性事务与柔性事务。
二、刚性事务 vs 柔性事务
1. 刚性事务
- 理论基础:严格遵循 ACID(原子性、一致性、隔离性、持久性)原则。
- 核心特点:追求强一致性。事务执行过程中会对涉及的资源加锁,参与者同步阻塞等待协调者的决策,隔离级别高。
- 代表方案:基于 XA 协议 的两阶段提交(2PC)、三阶段提交(3PC)。
- 优点:数据一致性最强,对业务代码侵入小。
- 缺点:性能差(同步阻塞),可用性低(协调者单点故障),扩展性弱,不适合高并发和长事务场景。
2. 柔性事务
- 理论基础:遵循 BASE(基本可用、软状态、最终一致性)理论。
- 核心特点:放弃强一致性,接受短暂的数据不一致(软状态),通过补偿或重试机制来达到最终一致性。通常不长期持有锁,并发度高。
- 代表方案:TCC、Saga、本地消息表、事务消息、最大努力通知。
- 优点:高性能、高可用、可扩展性好,非常适合微服务架构。
- 缺点:对业务有侵入(如TCC需要实现三个接口),开发复杂度较高,需要仔细处理幂等和补偿逻辑。
简单来说,刚性事务像“强锁”,确保数据100%精确但牺牲了速度;柔性事务像“协议”,允许暂时的误差但换来了系统的敏捷与健壮。在当今高并发、高可用的互联网系统中,柔性事务是更主流的选择。
三、刚性事务解决方案
1. 两阶段提交(2PC)
2PC是最经典的分布式事务协议,像一个谨慎的“指挥官”。
- 角色:一个事务协调者(TC) 和多个参与者(RM)。
- 过程:
- 准备阶段(投票):协调者询问所有参与者:“可以提交吗?”参与者执行事务操作(写本地undo/redo日志),锁定资源,但不提交,然后回复“同意”或“中止”。
- 提交阶段(执行):如果所有参与者都同意,协调者发送“全局提交”命令;否则发送“全局回滚”命令。参与者收到命令后执行最终提交或回滚,并释放资源锁。
- 缺点:
- 同步阻塞:在收到协调者指令前,参与者占用的资源锁不会释放,其他事务必须等待。
- 单点故障:协调者一旦宕机,参与者会一直处于不确定的阻塞状态。
- 数据不一致:在提交阶段,如果协调者发送部分提交指令后崩溃,会导致部分参与者提交,部分未提交。
2. 三阶段提交(3PC)
3PC是对2PC的改良,主要引入了超时机制,并将准备阶段一分为二。
- 过程:
- CanCommit:协调者询问参与者是否“具备提交条件”。此阶段仅做可行性检查,不锁定资源。
- PreCommit:如果所有参与者都回复Yes,协调者发送“预提交”指令。参与者此时才执行事务并锁定资源,然后返回确认。
- DoCommit:协调者根据预提交阶段的响应,决定最终提交或中止。
- 优点:降低了阻塞范围。在PreCommit阶段后,如果参与者收不到DoCommit指令,超时后可以自行提交,减少了长时间阻塞的风险。
- 缺点:在网络分区故障下,仍可能出现数据不一致。实现更为复杂,在实际生产环境中应用相对较少。
四、柔性事务解决方案
1. TCC(Try-Confirm-Cancel)
TCC可以理解为“业务层面”的两阶段提交,需要开发者显式实现三个业务方法。
- Try:资源检查和预留。例如:冻结库存、预扣账户余额(资金暂存于中间状态)。
- Confirm:确认执行业务。使用Try阶段预留的资源完成最终操作,此操作必须幂等。
- Cancel:取消业务。释放Try阶段预留的资源,进行回滚,此操作也必须幂等。
执行流程:
- 主业务服务发起全局事务,依次调用所有从服务的
Try接口。
- 若所有
Try成功,则调用各服务的Confirm接口进行确认。
- 若任意一个
Try失败,则调用所有已成功服务的Cancel接口进行补偿。
适用场景:对一致性要求较高且执行时间短的业务,如支付、下单。其缺点也很明显:业务侵入性高,每个服务都需要实现三个接口,设计和开发成本大。
2. Saga 模式
Saga模式将一个长的分布式事务拆分为多个连续的本地事务,并为每个本地事务设计一个对应的补偿事务。
- 协调方式:
- 编排(Choreography):事件驱动。每个服务执行完后,发布事件触发下一个服务;失败时,由监听器触发逆向补偿事件。无中心节点,但流程逻辑散落在各服务中。
- 协同(Orchestration):指令驱动。一个中央协调器负责按顺序调用各服务,并在失败时调用对应的补偿服务。逻辑集中,易于管理和监控。
- 优点:非常适合长事务、业务流程复杂的场景(如旅行预订:订票、订酒店、租车)。全程无锁,并发度高。
- 缺点:事务隔离性弱(可能出现脏读),补偿逻辑的设计和实现同样复杂,也需要保证幂等性。
3. 本地消息表(最终一致性)
这是一种非常朴素而可靠的方案,核心是利用本地数据库事务来保证消息投递。
- 原理:
- 事务发起方在同一个本地数据库事务中,完成业务操作并插入一条消息记录到本地消息表。
- 后台有一个定时任务,不断扫描本地消息表,将未发送的消息投递到消息队列。
- 消息消费者处理业务,成功后向MQ返回确认(ACK),发起方再删除本地消息。
- 若消费失败,MQ会重试,要求消费方实现幂等处理。
- 优点:实现简单,可靠性高,保证了业务和消息的本地原子性。
- 缺点:消息表与业务库耦合,会竞争数据库资源;定时扫描存在延迟;扩展性受数据库性能制约。
4. 事务消息(RocketMQ等)
这是对本地消息表的升级,将消息的持久化和状态管理职责转移给了消息中间件。
- 原理(以RocketMQ为例):
- 生产者发送“半消息”到MQ(此消息对消费者不可见)。
- MQ返回成功响应后,生产者执行本地事务。
- 根据本地事务结果,生产者向MQ发送
Commit或Rollback指令。
- 若MQ长时间未收到确认指令,会发起“事务回查”询问生产者本地事务的最终状态,以此决定消息是否投递。
- 优点:业务方无需自建消息表,与业务数据库解耦。
- 缺点:强依赖于消息中间件的事务消息特性,且实现逻辑相对复杂。
5. 最大努力通知
适用于对一致性要求最低的场景,目标不是保证“一定成功”,而是“尽最大努力”。
- 原理:服务A完成操作后,异步调用服务B的接口进行通知。如果失败,则按照策略(如1min,5min,10min…)反复重试。接收方B需要提供幂等接口,并且通常还需要有主动查询补偿的机制。
- 典型场景:支付结果回调通知、短信发送状态通知。
- 优点:实现极其简单,对业务侵入小。
- 缺点:无法保证数据的强一致或最终一致,业务方必须能接受通知可能永远失败的情况。
五、方案对比与选择建议

如何选择?这里有几点实用的建议:
- 追求高性能与高可用:如果你的业务可以接受秒级甚至分钟级的最终一致性,那么柔性事务是首选。其中,短事务、高性能场景(如支付)可选TCC;长流程、业务复杂的场景(如订单履约)可选Saga。
- 业务天然异步:如果流程本身可以异步化,比如订单生成后发送短信或积分,使用事务消息(如RocketMQ)是优雅的解耦方案。
- 系统简单,快速落地:对于中小型系统,不想引入复杂的中间件,本地消息表是经过验证的、简单可靠的方案。
- 强一致性是铁律:只有在像跨行转账这种对强一致性有绝对要求,且并发量不高的场景下,才值得考虑2PC/3PC,并承受其带来的性能代价。
没有“银弹”,每种方案都是在一致性、性能、复杂度和可用性之间做权衡。在实际架构设计中,深入理解业务需求是做出正确技术选型的第一步。如果你想更深入地探讨特定方案在Java微服务中的实现细节,或者与其他开发者交流实战经验,欢迎来云栈社区的相关板块一起学习成长。
|