1. 业务遇到的分布式事务问题

在数众项目中,创建任务的流程涉及多个服务间的协作。具体来说,portal端发起创建任务请求后,TCWServer会先创建唯一的stepId并写入数据库。紧接着,它会调用DataFlowServer服务,后者负责创建planId和planStepId并同样持久化。然后,TCWServer需要将stepId与DataFlowServer返回的planId、planStepId进行绑定,并最终将stepId返回给客户端,以便后续查询任务状态。
这里隐藏着一个典型的分布式一致性问题:如果客户端传递的任务参数有误或其他异常导致DataFlowServer服务创建planId、planStepId失败,那么TCWServer前期创建的stepId就无法与有效的plan信息关联。这个孤立的stepId就变成了“脏数据”。客户端拿到这个stepId去查询任务时,自然无法得到预期结果,从而导致业务出错。我们期望的是,TCWServer创建stepId和DataFlowServer创建planId、planStepId这两个动作,必须作为一个整体——要么全部成功,要么全部失败。
也许有人会想,调整调用顺序是否可行?比如让TCWServer先调用DataFlowServer,成功后自己再创建stepId并绑定。然而,这依然无法避免问题:如果TCWServer在创建或绑定stepId时失败,DataFlowServer已经创建好的planId和planStepId又会成为新的脏数据。
可能你会觉得,用户查询出错,大不了让他重新创建一次任务。这些脏数据似乎不会造成实质性影响,无非是数据库里多了几条无用记录。
但请考虑一个更敏感的业务场景:用户积分兑换礼品。在这个流程中,任何一步的失败都可能导致直接的业务损失。假设前面的礼品扣减库存操作成功了,但后续扣减用户积分却失败了,这相当于用户可以不付出任何代价就获得礼品。如果礼品库存和用户积分数据分属不同的数据库,甚至是由两个独立的服务来管理,我们该如何保证整个操作的原子性?或者说,如何确保在扣减积分失败时,之前成功的礼品扣减操作能够被可靠地回滚?

上述两个问题的本质,都是分布式系统中如何保证跨服务、跨数据源的事务一致性问题。
2. 本地事务

讨论事务,就绕不开著名的ACID四大特性:
- 原子性 (Atomicity):事务被视为一个不可分割的工作单元。事务中的所有操作要么全部成功,要么全部失败回滚。
- 一致性 (Consistency):事务执行前后,数据库必须保持一致的完整性约束状态。它确保数据从一个正确的状态转换到另一个正确的状态。
- 隔离性 (Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务。通常通过锁机制来实现不同程度的隔离级别。
- 持久性 (Durability):一旦事务被提交,其结果就是永久性的。即使系统发生故障,已提交的数据也不会丢失。
数据库通过undo/redo日志来保障原子性、一致性和持久性,而隔离性则依赖于锁机制。在大多数传统应用中,我们只操作单一的数据库,这种场景下的事务被称为“本地事务”。其ACID特性由数据库本身直接提供支持。
然而,随着微服务架构的普及,一个大型业务系统往往由多个独立的子系统构成,每个子系统拥有自己的数据库。一个完整的业务流程常常需要多个子系统协同完成,且这些操作需要在逻辑上形成一个整体事务。这种跨越多个独立资源(数据库、服务)的事务,就是我们今天要深入探讨的“分布式事务”。
3. 分布式事务典型场景
1. 跨库事务
- 垂直分库:不同的业务数据存储在不同的数据库中,一个应用的功能需要同时操作多个库。

- 水平分库:将单一数据库按规则拆分到多个库中。

2. 微服务/SOA跨服务调用
在微服务架构下,一个业务流程需要调用多个服务,每个服务可能操作自己的数据库。

既然目前多数数据库本身并不支持跨库事务,我们应该如何实现分布式事务呢?在分布式系统中,各个节点物理上独立,通过网络通信进行协作。传统事务机制可以保证单个节点上的数据操作满足ACID,但节点之间无法准确获知彼此的事务执行状态。
分布式事务处理的核心关键在于,必须有一种机制能够追踪事务在所有参与方(参与者)所做的动作。最终,关于提交或回滚的决定必须产生一个统一的结果——要么全部提交,要么全部回滚。
4. 分布式CAP、BASE理论与柔性事务
CAP理论

CAP理论指出,对于一个分布式计算系统来说,不可能同时满足以下三点:
- 一致性 (Consistency):所有节点在同一时刻看到的数据是相同的(强一致性)。
- 可用性 (Availability):每个请求都能收到一个(非错误)响应,但不保证它包含最新的写入。
- 分区容错性 (Partition tolerance):系统在遇到任何网络分区故障时,仍然能够对外提供满足一致性和可用性的服务。
分布式系统最多只能同时满足其中的两项。由于网络故障是常态,分区容错性(P)是必须保障的。而我们通常也无法放弃可用性(A),因此往往需要在实践中牺牲强一致性(C)。但这并不意味着我们放弃了数据一致性,而是退而求其次,追求最终一致性 (Eventual Consistency)——只要系统保证在一段可接受的时间后,数据最终能够达到一致的状态。
BASE理论
BASE理论是对CAP理论的延伸和实践,包含:
- 基本可用 (Basically Available):分布式系统出现故障时,允许损失部分可用性(如响应时间变长、功能降级)。
- 软状态 (Soft State):允许系统中的数据存在中间状态,并认为该状态不会影响系统的整体可用性。
- 最终一致性 (Eventual Consistency):强调系统保证数据在经过一段时间的同步后,最终能够达到一致的状态。
BASE理论面向的是高可用、可扩展的分布式系统,其思想与传统的ACID强一致性模型背道而驰。它通过牺牲强一致性来换取系统的高可用性。
刚性事务与柔性事务
- 刚性事务:满足ACID理论,追求强一致性。
- 柔性事务:满足BASE理论,追求基本可用和最终一致性。在互联网高并发场景下,柔性事务是更常见的选择。
5. 两阶段提交与三阶段提交协议
5.1 两阶段提交协议 (2PC)
两阶段提交协议的核心思想是引入一个协调者 (Coordinator) 来统一掌控所有参与者 (Participant) 的操作结果。协调者负责收集参与者的投票,并最终指示所有参与者是提交还是回滚。
两个阶段:
- 准备阶段 (Prepare Phase)
- 提交阶段 (Commit Phase)
1. 准备阶段
协调者向所有参与者发送“Prepare”请求。参与者收到请求后,执行本地事务操作,但并不提交,而是将操作结果(Undo和Redo信息)写入日志,进入“预备”状态。然后,参与者向协调者反馈响应:如果可以提交则返回“同意”,如果事务执行失败则返回“中止”。
2. 提交阶段
协调者根据第一阶段的反馈决定全局事务的走向:
- 情况一:全部同意
- 协调者向所有参与者发送“Commit”请求。
- 参与者收到请求后正式提交本地事务,并释放占用的资源。
- 参与者向协调者发送“提交完成”确认。
- 协调者收到所有确认后,完成全局事务。
- 情况二:任何参与者反对或超时
- 协调者向所有参与者发送“Rollback”请求。
- 参与者利用准备阶段写入的Undo日志执行回滚,并释放资源。
- 参与者向协调者发送“回滚完成”确认。
- 协调者收到所有确认后,取消全局事务。
所有参与者都必须实现prepare、commit、rollback三个接口。两阶段提交通过“延迟提交”的方式,将实际提交操作(第二阶段)缩短为一个极短的动作,从而降低了在分布式环境下提交失败的概率,尽力保证了分布式事务的原子性。
协议依赖于日志的持久化。如果参与者宕机重启,可以通过查询日志、协调者或其他参与者来恢复事务状态。
两阶段提交的缺陷:同步阻塞、单点故障、数据不一致

两阶段提交的主要问题在于它的同步阻塞。在准备阶段后,参与者的资源一直处于锁定状态,直到收到协调者的最终指令。如果协调者发生故障,整个系统可能进入阻塞。虽然可以引入协调者备份 (watchdog) 来接替故障的协调者,通过查询参与者状态来决定下一步操作,但这依然无法解决一个极端情况:
如果协调者在发出commit消息后立刻宕机,而唯一收到这条commit消息的那个参与者也跟着宕机了。那么,当watchdog和其他存活的参与者启动后,没有任何人知道协调者最后发出的是commit还是rollback指令,也无法知道那个宕机的参与者最终做了什么决定。系统将陷入既不能回滚(怕与宕机参与者的commit冲突),也不能强制提交(怕与宕机参与者的rollback冲突)的尴尬境地,除非那个宕机的参与者恢复。
5.2 三阶段提交协议 (3PC)
三阶段提交是2PC的改进版,将2PC的“准备阶段”再次拆分,形成了CanCommit、PreCommit、DoCommit三个阶段,并引入了超时机制来减少阻塞。

1. CanCommit阶段
协调者询问参与者是否可以执行事务提交。参与者根据自己的情况(如资源是否充足)返回“Yes”或“No”。此阶段不锁定资源。
2. PreCommit阶段
协调者根据第一阶段的反馈决定:
- 全部Yes:协调者发送
PreCommit请求,参与者执行事务操作(写日志,锁定资源),成功后返回ACK。
- 任何No或超时:协调者发送
abort请求,参与者中断事务。
3. DoCommit阶段
协调者根据第二阶段的反馈决定:
- 全部收到ACK:协调者发送
DoCommit请求,参与者正式提交事务,释放资源,返回ACK。
- 未全部收到ACK:协调者发送
abort请求,参与者利用Undo日志回滚,释放资源,返回ACK。
3PC的关键改进:
在DoCommit阶段,如果参与者长时间未收到协调者的指令(DoCommit或abort),它会默认执行提交。这是因为能够进入第三阶段,意味着所有参与者在第一阶段都回答了“Yes”,大家原则上都同意修改,所以提交的成功概率被认为很高。这避免了2PC中因协调者单点故障导致的长期资源锁定问题。
3PC的代价与问题:
3PC增加了一个网络往返(RTT),事务处理延迟比2PC更高。同时,其“超时默认提交”的策略也可能引入新的不一致:如果由于网络分区,协调者发出的abort请求未被某个参与者及时收到,该参与者在超时后执行了提交,而其他收到abort的参与者执行了回滚,就会导致数据不一致。
无论是2PC还是3PC,都无法在极端情况下(如网络脑裂)100%保证分布式一致性。事务管理器需要记录详细日志,并在必要时进行人工干预。
6. 分布式事务方案——基于两阶段提交的XA事务
XA是一个由Open Group定义的分布式事务处理(DTP)标准协议。它将事务参与者分为三个角色:
- 应用程序 (Application Program, AP):定义事务边界,发起全局事务。
- 资源管理器 (Resource Manager, RM):管理共享资源,如数据库、消息队列。主流的关系型数据库(如MySQL、Oracle)都实现了XA接口。
- 事务管理器 (Transaction Manager, TM):协调全局事务,负责与各个RM通信,决定事务的提交或回滚。

XA协议允许不同数据库(如MySQL和Oracle)参与同一个全局分布式事务。

XA的本质就是基于两阶段提交协议来实现分布式事务的。

XA事务成功与失败模型:


MySQL的InnoDB引擎从5.0.3版本开始支持XA事务,提供了如下SQL命令:

对于Java开发者,通常不直接使用这些SQL命令,而是通过JTA(Java Transaction API)规范来操作。应用服务器(如WebLogic)或第三方库(如Atomikos)提供了TM的实现。开发人员使用UserTransaction接口来开启、提交、回滚全局事务。
XA事务的优缺点:
- 优点:对业务代码侵入小(尤其是单体应用跨库场景),由数据库和中间件保障强一致性。
- 缺点:
- 同步阻塞与性能问题:在整个两阶段提交过程中,资源(如数据库行锁)会一直被持有,直到事务结束,在高并发场景下容易成为性能瓶颈,甚至引发死锁。
- 单点故障:TM存在单点问题。
- 对微服务支持不友好:XA本质是资源层的分布式事务,在微服务架构下,事务需要跨越多个服务(而不仅仅是多个数据库),协调与恢复变得异常复杂。
- 不支持非关系型数据库:许多NoSQL数据库并不支持XA协议。
因此,在微服务和互联网高并发场景下,传统的XA方案使用较少。
7. 分布式事务方案——TCC事务
TCC(Try-Confirm-Cancel)是一种业务层的分布式事务解决方案,主要解决跨服务调用场景下的一致性问题。
- Try阶段:完成所有业务检查,并预留必要的业务资源。例如,将账户状态从“正常”改为“冻结中”,或预扣库存。
- Confirm阶段:在Try成功的基础上,确认执行业务操作。使用Try阶段预留的资源,通常不会失败。此操作需保证幂等性。
- Cancel阶段:如果Try阶段有任一参与者失败,则取消所有参与者的资源预留,回滚到事务初始状态。此操作也需保证幂等性。

TCC与XA两阶段提交在思想上异曲同工:

- 阶段1 (Try/Prepare):尝试预留资源/询问是否可以提交。
- 阶段2 (Confirm, Cancel/Commit, Rollback):根据第一阶段结果,确认提交或取消/回滚。
TCC与XA的核心区别:

- 层面不同:XA是资源层(如数据库)的分布式事务,强一致性;TCC是业务层(服务接口)的分布式事务,最终一致性。
- 锁资源时长:XA在整个两阶段提交期间持续锁定数据库资源;TCC的Try、Confirm、Cancel通常是独立的本地事务,只在各自短暂时长内锁定资源,大大减少了资源锁定时间,提升了并发性能。
- 控制权:XA的两阶段对开发者透明;TCC则需要业务系统显式实现三个接口,开发成本更高。
TCC的优缺点:
- 优点:性能好,避免了XA的长时间资源锁定;适用于高并发场景。
- 缺点:
- 开发成本高:需要改造业务,为每个参与者服务实现Try、Confirm、Cancel三个接口,业务侵入性强。
- 实现复杂度高:一个健壮的TCC框架需要处理空回滚、防悬挂、幂等控制、故障恢复等一系列复杂问题。
- 缺乏成熟开源框架:目前业界缺乏被广泛采用的成熟开源TCC框架。阿里云虽推出了全局事务服务GTS,但并非开源产品。

TCC属于强一致性事务。在追求极致性能的场景下,我们是否可以退一步,接受短暂的不一致?这就是最终一致性事务的思路。
8. 分布式事务方案——最大努力通知
最大努力通知是一种简单的最终一致性方案,其核心是定期校对。它不保证消息的可靠投递,而是通过多次重试和提供校对接口来尽力达成一致。
实现方式:
- 业务主动方完成本地事务后,向被动方发送消息(允许丢失)。
- 主动方按设定规则(如1分钟、5分钟、10分钟后)重复通知N次。
- 主动方提供校对查询接口,被动方在未收到通知时可主动调用此接口查询业务状态。
- 被动方正常接收后,流程结束。
典型场景:短信通知、支付结果异步回调。

流程说明:
- 业务方提交短信发送请求。
- 短信平台接收请求,记录状态为“已接收”。
- 调用外部短信供应商接口,异步发送。
- 更新状态为“已发送”。
- 短信供应商异步回调通知结果(最多N次)。
- 短信平台根据回调更新最终状态(成功/失败)。
- 如果N次回调均失败,短信平台可定期调用供应商的查询接口进行主动校对。
这种方案适用于对时间不敏感、可接受一定延迟的业务。
9. 分布式事务方案——可靠消息(推荐方案)
可靠消息模式的核心思想是将一个大分布式事务拆分成多个本地小事务,并通过可靠的消息传递来驱动这些本地事务的最终执行,从而保证最终一致性。它需要解决两个核心问题:消息的可靠投递和消费的幂等性。
首先,如何保证主业务事务提交与消息发送这两个操作的原子性(即:要么都成功,要么都失败)?
错误示范:
public void trans(){
try{
//1.操作数据库
bool result = dao.update(model); // 操作数据库失败,会抛出异常
//2.如果第一步成功 则操作消息队列(投递消息)
if(result){
mq.append(model); // 如果投递消息失败,方法内部会抛出异常
}
}catch(Exception ex){
rollback(); // 如果发生异常 则回滚
}
}
这段代码的问题在于网络不确定性(两将军问题):mq.append()调用失败,我们无法区分是消息中间件真的没收到消息,还是消息已成功存入但返回响应时网络失败。如果此时回滚数据库,可能造成数据不一致(消息已发出,业务却回滚了)。此外,网络操作放在数据库事务内,可能导致长事务,阻塞数据库。
方案一:本地消息表
此方案的核心是将消息和业务数据保存在同一个数据库事务中。

操作步骤如下(以转账为例):
begin transaction;
-- 1. 扣减A账户余额(业务操作)
update User set account = account - 100 where userId = 'A';
-- 2. 插入一条待发送的消息记录(消息操作)
insert into message(userId, amount, status) values('A', 100, 1);
commit transaction;
- 在同一个本地事务中,完成业务操作并插入一条消息记录到本地消息表。
- 事务提交后,立即尝试将消息发送到消息队列。如果发送成功,则删除或更新本地消息表中的对应记录。
- 由一个独立的“消息恢复服务”定时扫描本地消息表中状态为“待发送”的消息,重新投递到消息队列,直到成功为止。
这个方案保证了“业务处理”和“消息记录”的原子性,但增加了业务系统的复杂度(需要维护消息表和一个后台任务)。
方案二:MQ事务消息(推荐)
为了解耦,RocketMQ等消息中间件提供了“事务消息”特性,替业务方完成了“本地消息表”和“扫描重试”的工作。
- 半消息 (Half Message):消息发送到MQ Server,但处于暂不可投递状态,消费者看不到。
- 消息回查 (Transaction Check):MQ Server长时间未收到半消息的确认指令,会主动回查生产者,询问该消息的最终状态(提交或回滚)。


交互流程:
- 生产者发送半消息到MQ Server。
- MQ Server返回半消息发送成功响应。
- 生产者执行本地事务。
- 生产者根据本地事务执行结果,向MQ Server发送Commit或Rollback指令。
-
- Commit:MQ Server将半消息转为正式消息,投递给消费者。
- Rollback:MQ Server删除半消息,不投递。
- 如果MQ Server长时间未收到步骤4的指令,会主动回调生产者的检查接口,询问消息状态。
- 生产者检查本地事务状态,并返回Commit或Rollback。
- MQ Server根据最终收到的指令,执行步骤5的操作。
消息投递的可靠性保证:
消息中间件基本都能做到 At Least Once(至少投递一次)。消费者消费成功后,需要向MQ返回ACK确认。如果MQ未收到ACK(消息丢失或确认丢失),会进行重试投递。这就要求消费者的业务逻辑必须实现幂等性,以应对可能出现的重复消息。
如果消费者消费持续失败,达到重试上限后,消息会进入死信队列,需要人工干预进行最终处理。人工干预是保障最终一致性的最后一道防线。
可靠消息方案通过异步化和解耦,在保证数据最终一致性的同时,提供了较高的系统性能和吞吐量,是当前互联网分布式系统中非常推荐的一种实践方案。更多关于系统设计和分布式架构的深入讨论,欢迎访问 云栈社区 进行交流。