想象你在筹备一桌十五人的年夜饭。你一个人做不完,于是请来了三位主厨:一位负责红烧肉,一位负责清蒸鱼,一位负责炖汤。菜必须同时上桌,而且要么全部成功、要么全部取消——绝对不能出现鱼上了但肉没熟的局面。更麻烦的是,三位主厨分别在三个不同的厨房里,你看不到对方的灶台,只能靠打电话沟通。 这就是分布式事务要解决的核心问题:多个独立节点(主厨)协同完成一个整体操作(一桌菜),且必须保持全部成功或全部失败。当你的系统从单机数据库扩展到微服务架构,业务操作不再只落在一张表里,而是横跨订单服务、库存服务、支付服务等多个独立服务,每个服务“各炒各的菜”,此时保证数据一致性就成了最棘手的挑战。 本文会用“年夜饭协作”这条故事线,带你走完分布式事务的经典之路:从两阶段提交(2PC)的“统一请示、统一开火”,到三阶段提交(3PC)的“三次确认、超时兜底”,再到 Saga 和 TCC 的“流水线调整、坏了就补”。读完你就明白,为什么说分布式事务本质上是“在不可靠的网络上报团取暖”。

岗位关联

分布式事务能力是架构师和中高级后端工程师的分水岭。掌握它,你才能在技术评审中拍板“这笔跨服务操作该用 2PC 还是 Saga”,在面试中被问到“如何保证支付不掉单”时给出有深度的答案。

目录
  1. 可视化演示
  2. 事务与ACID重启
  3. 两阶段提交2PC
  4. 三阶段提交3PC
  5. Saga补偿事务
  6. TCC模式精讲
  7. 最终一致性落地
  8. 技术选型与权衡
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示两阶段提交(2PC)、TCC补偿事务与Saga补偿链的协调流程,交互切换查看不同事务模式的执行与回滚机制。

本节配有交互动效(进入视口后自动加载,约 3KB)。

2PC:两阶段提交协议 协调器发送Prepare所有参与者投票锁定资源 全部Yes则Commit任一No则Abort回滚 latency=2RTT+n 同步阻塞协调器单点故障风险高

事务与ACID重启

年夜饭协作的第一课,是搞清楚“一桌菜”到底意味着什么。在单机数据库里,你一个人掌勺——事务(Transaction)就是一组不可拆分的工作:要么全部完成,要么全部不做。数据库用 ACID 四性来约束这个过程:原子性(Atomicity)要求所有步骤像一枚硬币,不能掰成两半;一致性(Consistency)保证操作前后数据都符合业务规则,比如转账后总金额不变;隔离性(Isolation)让并发的几个事务互不偷看对方的半成品;持久性(Durability)则承诺只要提交成功,数据就不会因断电而丢失——如同菜端上桌就不会凭空消失。

单机事务的幸运在于,所有操作都在一个数据库进程内,锁、日志、回滚段都由同一个引擎管理。这就像你一个人在自己的厨房里,灶台、食材、调料尽在掌握,任何一步炒糊了,倒掉重来就是。

可一旦进入微服务架构,情况骤变。“一桌菜”分布在三个厨房里,由三位主厨分别掌勺。你的“事务”要跨进程、跨网络、跨数据库——ACID 中原子性瞬间变得艰难:你无法让三个独立的数据库共享同一个回滚日志。这就是分布式事务诞生的根本原因:用协调协议在多个独立节点之间重建“原子性幻觉”。

从另一个角度看,这个问题也可被理解为分布式系统中的经典悖论——你无法既保证“所有节点同时看到一致状态”,又保证“系统随时可用”。分布式事务的种种方案,本质上都是在这个矛盾中选择不同的妥协点。

所以,当你读到下文的所有协议时,请始终记得:每一份“一致性”的获得,都以某种“可用性或性能”为代价。三位主厨能否高效协作,取决于你选择什么样的沟通方式。

单机事务的原子性保证度×分布式可用性保证度≤1\text{单机事务的原子性保证度} \times \text{分布式可用性保证度} \leq \text{1}
这个式子并非严格的数学定理,而是一种直觉框架:一致性越强(越高),分布式系统的可用性牺牲往往越大。它提醒你——没有免费的午餐。
  1. 单机原子性单个数据库内,事务的所有读写在同一个引擎里完成,靠 undo/redo 日志实现全有或全无。
    Atomicity:transaction→commit | rollback\text{Atomicity:} \quad \text{transaction} \to \text{commit | rollback}
  2. 分布式可用性当多个节点通过网络协作时,网络不可靠成为常态,节点可能宕机、消息可能丢失。
    Availability:P(node failure)>0\text{Availability:} \quad P(\text{node failure}) > 0
  3. 一致性权衡想在分布式环境下保持强一致性,往往需要牺牲部分可用性(比如阻塞等待),反之亦然。
    Consistency+Availability≤Distributed Reality\text{Consistency} + \text{Availability} \leq \text{Distributed Reality}

两阶段提交2PC

两阶段提交是分布式事务最经典的起点,思路极其直白:先问一圈,再统一开火。回到年夜饭场景,你就是总厨师长(协调者),三位主厨(参与者)分别在三个厨房。你拿起电话打给每一位主厨:“给你 30 分钟,把这道菜的食材备好、锅烧热,能开始炒吗?”如果三位都回答“能”,你再次逐一打电话:“现在,全体开火!”这就是两阶段提交的两轮通信——准备阶段和提交阶段。

在第一阶段(准备阶段),协调者向所有参与者发送准备请求(prepare),参与者各自执行事务操作但不提交,而是将资源锁定并回写 undo/redo 日志,然后回复“就绪”或“失败”。这里的“锁定”就像主厨把食材切好、调料摆齐,暂停在灶台前——食材被占用了,别人不能碰。只要有任何一位主厨回复“不行”(比如发现缺料),协调者就广播“全体取消”(abort),各参与者回滚已做的操作,释放锁,就像把切好的菜收回冰箱、锅底洗干净。

在第二阶段(提交阶段),协调者收到所有参与者的“就绪”后,广播提交指令(commit)。参与者收到后正式提交事务、释放资源,并回复确认(ack)。此时才真正“开火出菜”。

两阶段提交通过引入“协调者”这一角色,用两次广播换取全局原子性。它的核心价值在于:任何一个参与者都有否决权,只要有一方表示“不能提交”,整个事务就回滚——这保证了年夜饭不会出现“鱼上了但肉没熟”的残局。

然而 2PC 的致命弱点同样明显:同步阻塞和协调者单点故障。 同步阻塞是指,准备阶段所有参与者都已锁定资源,此时它们必须阻塞等待协调者的最终指令——如果协调者永久宕机,参与者就只能一直“举着锅铲”僵在那里,资源被无限期占用。更糟的是,协调者本身是单点,它挂了,整个协议就失去指挥。这个弱点在下面的时空图中更为直观。

想象你是那位等待电话指令的主厨:菜都切好了、锅也烧热了,但总厨师长的电话一直打不通。你不能把菜下锅,也不能收摊走人——只能干等。两阶段提交的“一致性”就建立在这种“大家齐步走”的强制等待之上,它解决了一致性,却牺牲了可用性。

尽管 2PC 有诸多问题,它仍是分布式事务的基石。理解它,你才能理解 3PC 为什么多一次预检,Saga 为什么完全放弃“锁”。下一节我们看看,如何给 2PC 打上补丁。

2PC 时间开销=2×RTT+参与者本地事务执行时间\text{2PC 时间开销} = 2 \times RTT + \text{参与者本地事务执行时间}
每次跨节点通信都有往返延迟 RTT,2PC 至少要两轮。这个公式帮你估算一个分布式事务延迟的下限。
  1. 两轮通信协调者到参与者之间的往返次数决定了协议延迟。
    Phase2:commit→ack\text{Phase}_2: \text{commit} \to \text{ack}
  2. 同步阻塞参与者在准备阶段后若未收到指令,必须一直持有锁。
    blocking time=waiting for coordinator\text{blocking time} = \text{waiting for coordinator}
  3. 单点故障协调者故障时,所有参与者无法达成共识。
    P(coordinator down)⇒stuckP(\text{coordinator down}) \Rightarrow \text{stuck}

三阶段提交3PC

三阶段提交(3PC)是对 2PC 阻塞问题的一次改造。它的思路可以理解为:在开火之前,多进行一次“预检”,并且给每个人装上“超时自动决策”的保险。回到年夜饭:总厨师长先打电话问“都能做吗?”(CanCommit),大家确认后,再问“那我倒数三个数,大家准备好!”(PreCommit),最后才说“开火!”(DoCommit)。关键变化是:如果在任何一阶段超时未收到指令,参与者不再傻等,而是按预设策略自行处理。

3PC 的三个阶段如下。阶段一 CanCommit:协调者向所有参与者发送“是否可以提交事务?”,参与者只需检查自身状态(是否资源充足、是否处于可执行状态),回复 Yes 或 No。这一阶段不锁定资源,类似问主厨“你原料够不够”——主厨只需要看一眼冰箱,不必动手。

阶段二 PreCommit:如果所有参与者都回复 Yes,协调者发送“预提交”指令。参与者收到后执行事务操作、写入日志并锁定资源,然后回复“已预提交”。注意到这里仍没有真正提交,但资源已被锁定。难点在于:协调者收到所有“已预提交”后,如果一切正常,才发送“正式提交”指令。

阶段三 DoCommit:参与者收到“正式提交”后,真正提交事务并释放资源。如果协调者在阶段三超时未收到指令,参与者不会无限等待——它会根据自己当前的状态决定:如果已经完成预提交,基于“多数派已就绪”的假设,直接提交;如果还未开始预提交,则中止事务。

3PC 比 2PC 多的核心机制就是超时与降级策略:参与者在等待超时后可以自行选择提交或中止,而不是死等协调者。这显著降低了同步阻塞时间。同时,CanCommit 阶段在事务执行前先行确认,避免了资源被无意义锁定。

但 3PC 也不是银弹。它引入了新的不一致窗口:如果一个参与者超时后选择提交,而其他参与者选择中止,系统就出现了不一致。本质原因是,分布式系统的网络分区和消息延迟是无法完全消除的——这正是著名的 FLP 不可能定理所揭示的:在异步网络中,不存在同时满足“安全性”和“活性”的确定性共识算法。3PC 只是把问题概率降低,而非根除。

你已经看到:分布式事务的每一步演进,都是在“一致性保证”与“可用性保障”之间的再平衡。3PC 用“多一次通信”换“少一些阻塞”,而接下来要讲的 Saga 则干脆换了一条完全不同的赛道——不再追求强一致,而是用“补偿”来收拾残局。

P(3PC 不一致)=P(网络分区)×P(超时后决策分歧)P(\text{3PC 不一致}) = P(\text{网络分区}) \times P(\text{超时后决策分歧})
这个式子表明,3PC 的不一致风险来源于极端网络场景,比 2PC 的“必然阻塞”前进了一大步。
  1. CanCommit 预检参与者只反馈自身能否执行,不锁资源。
    CanCommit?→Yes/No\text{CanCommit?} \to \text{Yes/No}
  2. PreCommit 预提交执行事务操作并锁定资源,但尚未真正提交。
    PreCommit→Ready for commit\text{PreCommit} \to \text{Ready for commit}
  3. DoCommit 正式提交真正提交并释放资源。
    DoCommit→Committed\text{DoCommit} \to \text{Committed}
  4. 超时降级参与者在超时后不再盲等,而是依据已处阶段做出决策。
    Timeout→decide(local state)\text{Timeout} \to \text{decide(local state)}

Saga补偿事务

如果说 2PC/3PC 是“齐步走”式的强一致协议,Saga 则像一份允许临时乱序、最终总能调整好的流水菜单。回到年夜饭:你不再是让三位主厨同时开火,而是设计一份菜单流程:先上冷盘,再上热菜,最后上甜汤。每一步由一位主厨单独完成,不需要锁定所有食材。但问题是:如果热菜做到一半发现鱼不新鲜,你该怎么办?Saga 的答案是:倒退回去,把已经上桌的菜撤下来——撤销已经完成的步骤,这个过程叫作“补偿”。

Saga 是一种长事务解决方案,它的核心思想是:将一个分布式大事务拆分为一系列有顺序的本地事务,每个本地事务都有对应的补偿事务。正常流程下,依次执行正向事务 T₁、T₂、T₃……;如果某步 Tₖ 失败,则逆序执行补偿事务 Cₖ₋₁、Cₖ₋₂……来撤销之前已提交的步骤。

Saga 有两种执行模式:编排(Orchestration) 与 协同(Choreography)。编排模式中,一个中心协调器(类似总厨师长)显式地指挥每个步骤:“你做冷盘、你做热菜、你做甜汤”,并跟踪当前状态,失败时主动发起补偿。协同模式中则没有中心指挥,每个服务完成自己的任务后,通过事件触发下一个服务,类似流水线上的工人听到上一环节的信号就自动开工;补偿也通过事件链逆流传播。

Saga 的补偿操作必须遵循可补偿(compensatable)原则,即每个正向操作都有对应的、可执行的逆向操作。举例:预订酒店(正向)→ 取消预订(补偿);扣款(正向)→ 退款(补偿)。这些操作是长期的、可异步执行的,且不依赖全局锁——这正是 Saga 能应对长事务的关键。

与 2PC 相比,Saga 放弃了隔离性:中间步骤的结果对其他事务是可见的。在年夜饭的比喻中,这意味着“冷盘已经上桌,客人可以开吃,即便后面热菜失败,冷盘被撤下,客人也已经看到了冷盘”。在业务上,这通常是可以接受的——比如一个旅游预订流程,用户先订机票、再订酒店,最后支付,如果酒店预订失败,系统自动取消机票,用户中途会看到“已订机票”但最终被退订。

Saga 的价值在于:它不需要参与者长时间持有资源锁,每个子事务提交后立即释放资源,系统的并发度和可用性大幅提升。代价是引入了补偿机制的复杂度,以及中间状态对外可见带来的业务妥协。

Saga 的哲学是“接受中间状态的不完美,但保证终态正确”。如果你能接受这一点,它会是许多跨服务长流程的最佳选择。而 TCC 则是那些“必须始终保持资源可控”的场景下的另一种答案。

Saga Execution:T1,T2,…,Tn→failure at TkCk−1,…,C1\text{Saga Execution:} \quad T_1, T_2, \ldots, T_n \xrightarrow{\text{failure at } T_k} C_{k-1}, \ldots, C_1
一个正向事务流 T₁→T₂→…→Tn,如果 Tₖ 失败,就逆序补偿前 k-1 步。
  1. 正向链路一系列本地事务顺序执行,每步提交后释放资源。
    T1→T2→⋯→TnT_1 \to T_2 \to \cdots \to T_n
  2. 补偿链路逆向执行补偿事务,撤销已完成步骤。
    Ck−1→⋯→C1C_{k-1} \to \cdots \to C_1
  3. 编排模式中心协调器控制所有步骤。
    Orchestrator→Ti, on fail→Ci\text{Orchestrator} \to T_i,\ \text{on fail} \to C_i
  4. 协同模式各服务通过事件异步串联。
    Ti→eventTi+1T_i \xrightarrow{\text{event}} T_{i+1}

TCC模式精讲

TCC(Try-Confirm-Cancel)是另一种补偿型事务方案。如果说 Saga 是“流水线式地事后补救”,TCC 就像预订婚宴酒席:先付定金锁定场地(Try),确认宾客数量后补尾款(Confirm),如果婚礼取消,则退还定金、释放场地(Cancel)。每一步都围绕“资源”的预留与释放展开。

TCC 将每个分布式操作拆分为三个方法。Try:尝试执行,完成业务检查并预留资源,但不真正执行业务变更。比如下单时,Try 阶段检查库存并锁定 10 件商品,但订单状态仍为“待确认”;在转账场景中,Try 阶段冻结转出金额。Confirm:确认执行,真正执行业务变更。Try 成功后,Confirm 将“锁定”变为“扣减”——库存真正减少、转账真正完成。Cancel:取消执行,释放 Try 阶段预留的资源。如果任意一个参与者 Try 失败或 Confirm 失败,协调者调用所有已成功 Try 的参与者的 Cancel 方法,释放预留资源。

TCC 的关键优势是资源全程可控:从 Try 到 Confirm/Cancel 之间,资源始终处于被锁定状态,不会出现 Saga 中“中间状态对外可见”的问题。这在财务对账、库存扣减等场景中极其重要。

TCC 的难点在于业务侵入性强:原本只需要一个更新库存的 SQL,现在必须编写 Try、Confirm、Cancel 三个方法,且要处理大量边界情况。特别是 Confirm 和 Cancel 必须保证幂等——因为网络重试可能导致重复调用,幂等性确保重复调用与单次调用结果一致。设计上常用唯一事务 ID + 状态表来去重。

与 Saga 相比,TCC 更强一致(Try 阶段锁资源,几乎无中间状态泄露)但成本更高(三方法编码、幂等控制、补偿逻辑)。TCC 适合资源竞争激烈、需要预防并发超卖的场景,如秒杀系统、库存扣减、账户余额变更。

TCC 是“用复杂度换确定性”的典型。想要资源全程受控,就要承担三方法编写的高昂成本。接下来,我们把视野拉高,聊聊当系统规模大到一定程度时,不追求强一致、只追求“最终会一致”的另一种宇宙。

TCC State Machine:Try→{Confirmif all Try succeedCancelif any Try fails\text{TCC State Machine:} \quad \text{Try} \to \begin{cases} \text{Confirm} & \text{if all Try succeed} \\ \text{Cancel} & \text{if any Try fails} \end{cases}
TCC 的状态机看似简单,但真实落地时,Confirm/Cancel 的幂等和异常恢复才是工程重头戏。
  1. Try 资源预留每个参与者尝试执行业务检查并锁定资源。
    Try:check+lock(resource)\text{Try:} \quad \text{check} + \text{lock(resource)}
  2. Confirm 确认执行所有 Try 成功后,对每个参与者确认业务变更。
    Confirm:lock→deduct\text{Confirm:} \quad \text{lock} \to \text{deduct}
  3. Cancel 取消释放任一步失败,逆序释放已预留资源。
    Cancel:release(locked resource)\text{Cancel:} \quad \text{release(locked resource)}
  4. 幂等保证Confirm/Cancel 重复执行结果一致。
    idempotent:f(f(x))=f(x)\text{idempotent:} \quad f(f(x)) = f(x)

最终一致性落地

在真实的互联网场景中,高并发、高可用往往比强一致更紧迫。这时候,你需要的不是“所有人同时看到正确数据”,而是“所有人都能正常干活,数据早晚会正确”。这就是最终一致性(Eventual Consistency)。回到年夜饭比喻的变体:你不再要求所有菜同时上桌,而是约定“菜做好一道就上一道,大家最终都能吃上整桌菜”。只要上菜的流程可靠,客人最后总能吃完全部菜。

最终一致性的经典落地方式是消息队列(MQ)+ 本地消息表。以电商下单为例:订单服务创建一个订单,同时向库存服务发起扣减请求。核心技巧在于,订单服务和消息表在同一个本地数据库中:创建订单时,同时在本地消息表中插入一条“扣减库存”的消息,二者在同一个本地事务中提交,保证订单与消息要么同时成功、要么同时失败。

接着,一个异步任务扫描本地消息表,将待发送消息投递到 MQ;库存服务消费消息执行扣减;扣减成功后,库存服务回调订单服务确认;订单服务收到确认后,将消息表对应记录标记为“已处理”。如果投递失败,异步任务会重试;如果库存服务处理失败,也会发一条失败消息,订单服务据此做反向操作。

这套机制的关键在于消息的可靠投递与幂等消费。网络可能中断、消费者可能宕机,因此 MQ 通常提供“至少一次”投递保证,消费者则必须做好幂等处理——同样的扣减请求重复执行,不能造成重复扣款。实践中通过业务唯一 ID(如订单号)做去重即可。

最终一致性的工程支柱还有定时对账:即使消息机制出现bug,系统定期比对订单数据和库存数据,发现不一致后通过补偿任务修复。对账是兜底方案——可以想象成饭局结束后,总管盘点每桌的菜是否齐全,差了哪道菜再回厨房补做。

最终一致性并非不做补偿,而是把补偿异步化、事件化、自动化,用“消息链”替代“强同步协调”。它换来的,是系统的高吞吐与高可用(下单请求无需等待库存扣减同步完成),代价是短暂的数据不一致窗口(用户看到“已下单”但“库存暂未扣减”)。

最终一致性是现代互联网系统的“常态”。它不追求完美的瞬间,而追求可靠的收敛。理解了这个思想,我们就能站在务实的角度,讨论最后一个问题:面对具体业务,你到底该选哪种方案?

Eventual Consistency:stateA→async message + retrystateB\text{Eventual Consistency:} \quad \text{state}_A \xrightarrow{\text{async message + retry}} \text{state}_B
系统状态 A 通过异步消息、重试与补偿,最终收敛到状态 B。
  1. 本地事务表业务操作与消息写入同一库,共用一个本地事务。
    order←transaction→msg\text{order} \leftarrow \text{transaction} \rightarrow \text{msg}
  2. 异步投递后台任务扫描消息表,投递到 MQ。
    msg table→scanMQ→consumeservice\text{msg table} \xrightarrow{\text{scan}} \text{MQ} \xrightarrow{\text{consume}} \text{service}
  3. 幂等消费消费者用业务 ID 去重,保证重复消息不重复处理。
    idempotent:dedup by order_id\text{idempotent:} \quad \text{dedup by order\_id}
  4. 定时对账周期性比对数据一致性,发现差异则人工或自动补偿。
    reconciler:order⨁inventory=0 (expected)\text{reconciler:} \quad \text{order} \bigoplus \text{inventory} = 0 \ \text{(expected)}

技术选型与权衡

当你在架构评审会上被问到“这个跨服务操作怎么保证一致性”,你要做的不是背诵方案,而是基于业务场景做权衡。这就好比总厨师长决定采用哪种协作方式,完全取决于这顿饭的性质:如果是国宴(金融转账),宁可通过阻塞等待保证分秒不差;如果是自助餐(电商订单),允许菜陆续上桌,保证客人能吃到就好。

首先明确一个核心原则:没有银弹。2PC、3PC、Saga、TCC、最终一致性——每种方案都在一致性、可用性、性能、复杂度四个维度上各有取舍。选型的起点是问自己三个问题:业务是否能容忍短暂的不一致窗口?事务的平均时长是秒级还是分钟级?系统对可用性的要求有多高?

强一致需求优先的典型场景是金融转账、账户余额变更。A 账户扣款与 B 账户入款必须在同一瞬间对外可感知一致,任何中间状态都是不可接受的。这类场景通常选择 2PC 或 TCC——2PC 简单直接,但在高并发下阻塞风险高;TCC 更灵活,能控制资源粒度,缺点是编码复杂。如果业务量不大但一致性要求极严,2PC 也足够。

高可用优先的典型场景是电商下单、订单状态流转。用户下单时系统必须快速响应,不能因为库存服务慢就阻塞下单。此时 Saga 是最优解:下单、锁库存、支付、发货各为独立本地事务,通过编排或消息链串联,失败则逆序补偿。用户可能会遇到“订单已创建但支付后库存不足,系统自动退款”的体验,但这在电商场景是可接受的。

资源竞争激烈的场景,如秒杀系统、机票预订,需要防止超卖——两个用户同时抢最后一件商品,只能有一个人成功。TCC 是最佳选择:Try 阶段锁库存,保证了“确定性”,Confirm 阶段真正扣减,Cancel 释放。相比之下,Saga 在资源预留方面较弱(因为其子事务直接提交),容易超卖。

最终一致性则适用于对实时性要求不高的场景:如积分累计、通知发送。这些场景即使延迟几秒甚至几分钟,业务上完全无感,用 MQ + 本地消息表即可。

最后,用一张直觉决策表总结(不追求完备,只求建立直觉):

分布式事务没有“标准答案”,只有“权衡答案”。下次面对这类问题,试着先把业务需求翻译成“一致性/可用性/性能/复杂度”四个评分维度,再选择对应方案。这不仅能让你的技术判断有章法,也能让同事和面试官感受到你的架构思维。

方案选择倾向≈一致性要求×资源竞争激烈度可用性要求×吞吐要求\text{方案选择倾向} \approx \frac{\text{一致性要求} \times \text{资源竞争激烈度}}{\text{可用性要求} \times \text{吞吐要求}}
分子越大越靠 2PC/TCC,分母越大越靠 Saga/最终一致性。这只是一个帮助思维的近似式,真正的决策还要考虑团队技术积累和运维成本。
  1. 一致性权重业务对“瞬间一致”的敏感度。
    consistency need=high, mid, low\text{consistency need} = \text{high, mid, low}
  2. 可用性权重系统对故障容忍度的要求。
    availability need=D, C, B, A\text{availability need} = \text{D, C, B, A}
  3. 决策方向高一致 → 2PC/TCC;高可用 → Saga/最终一致。
    CneedAneed⇒{强一致方案≫1最终一致方案≪1\frac{C_{\text{need}}}{A_{\text{need}}} \Rightarrow \begin{cases} \text{强一致方案} & \gg 1 \\ \text{最终一致方案} & \ll 1 \end{cases}

AI 应用场景

分布式推荐系统的特征更新一致性

阶段:特征平台建设 / 模型在线推理前的数据同步

解决的问题:机器学习特征平台由多个服务协作生成样本特征:用户画像服务更新标签、行为服务写入最新点击序列、特征拼接服务组合特征。在传统架构中,各服务独立写入特征库,常出现“标签已更新但行为序列还没到”导致训练样本错乱。采用 Saga 编排模式,将特征更新拆为“更新画像→写入行为→拼接特征”三步,任一步失败则逆序补偿(回滚标签、清理行为记录),保证最终每个样本的特征来自同一时刻的一致性快照。这保证了模型训练的输入质量,避免“脏特征”导致模型精度下降。

具体案例:某短视频平台的实时推荐特征平台,每日处理数十亿特征写入。早期使用 MQ 异步通知各服务更新,频繁出现标签与行为特征时间戳错位。改为 Saga 编排后,增加了“一致性版本号”贯穿链路,任一环节失败即整体补偿重试,特征一致率从 96.2% 提升至 99.98%。相关团队还建立了对账任务,每 10 分钟扫描一次版本号断裂的样本并自动修复。

大模型训练数据流水线的失败恢复

阶段:数据工程 / 训练数据预处理管道

解决的问题:大模型训练的数据预处理是一条多阶段流水线:原始日志清洗→去重→质量过滤→Tokenize→写回存储。若在第 4 步失败,传统做法是整条流水线重跑,浪费大量算力。借鉴 Saga 的补偿思想,每个处理阶段设计为独立事务并记录中间状态,失败时仅重跑后续阶段,已完成阶段保留缓存。类似 TCC 的资源预留思想,在 Tokenize 前先预留存储空间,确认写入成功后再释放,避免写满磁盘。

具体案例:某公司训练百亿参数模型时,预处理流水线每日处理 50TB 文本。引入“阶段状态机 + 补偿恢复”后,流水线失败恢复时间从 2 小时降至 15 分钟。关键设计是每个阶段输出带版本号,补偿逻辑仅回滚当前阶段的临时文件,而保留上游已完成的中间结果,大幅减少无效计算。

智能风控系统的跨服务决策一致性

阶段:模型推理 / 决策引擎与业务系统的协同

解决的问题:风控系统对每笔交易做实时评分,需要同时读取交易流水服务(新增交易记录)、用户画像服务(读取历史行为特征)、规则引擎(命中风控规则),三者需要一致的数据视图。当用户画像更新与交易写入跨服务发生时,采用 TCC 模式管理“风控特征快照”:Try 阶段锁定交易 ID 与画像版本,Confirm 阶段将二者绑定写入风控决策表,Cancel 阶段释放锁定。确保风控模型永远基于同一版本的特征组合做判断,避免因特征错位导致误杀或漏杀。

具体案例:某支付平台的风控系统,交易评分请求高峰达每秒 8 万次。在不引入全局锁的前提下,使用 TCC 对“交易流水 + 用户特征版本”做一致性绑定,决策不一致率从 0.3% 降至 0.003%。所有 Confirm/Cancel 操作均设计为幂等,配合 Redis 分布式锁实现资源预留。

Python 演示

python79 行
import randomimport time  class Participant:    """模拟一个分布式事务参与者(数据库节点)"""     def __init__(self, name, fail_prob=0.2):        self.name = name        self.fail_prob = fail_prob        self.prepared = False        self.committed = False        self.aborted = False     def prepare(self):        """第一阶段:执行本地事务但先不提交,返回是否可提交"""        time.sleep(0.1)  # 模拟网络 RTT        if random.random() < self.fail_prob:            print(f"[{self.name}] 本地资源不足,准备失败")            return False        self.prepared = True        print(f"[{self.name}] 事务已执行,资源已锁定,返回就绪")        return True     def commit(self):        """第二阶段:正式提交本地事务"""        if not self.prepared:            raise RuntimeError(f"[{self.name}] 未准备完成,不能提交")        time.sleep(0.1)        self.committed = True        print(f"[{self.name}] 已提交,资源已释放")     def rollback(self):        """回滚本地事务并释放资源"""        self.aborted = True        self.prepared = False        print(f"[{self.name}] 已回滚,资源已释放")  class Coordinator:    """两阶段提交协调者"""     def __init__(self, participants):        self.participants = participants     def run_two_phase_commit(self):        """执行两阶段提交,返回全局事务是否成功"""        print("\n===== 第一阶段:准备阶段 =====")        votes = []        for p in self.participants:            ok = p.prepare()            votes.append(ok)        if not all(votes):            print("\n===== 有参与者准备失败,执行全局回滚 =====")            for p in self.participants:                p.rollback()            return False         print("\n===== 第二阶段:提交阶段 =====")        for p in self.participants:            p.commit()        return True  def main():    # 固定随机种子,便于观察不同场景    random.seed(3)    participants = [        Participant("订单库"),        Participant("库存库", fail_prob=0.5),        Participant("支付库"),    ]    coordinator = Coordinator(participants)    success = coordinator.run_two_phase_commit()    print(f"\n全局事务结果: {'成功' if success else '回滚'}")  if __name__ == "__main__":    main()
目的:演示 2PC 的两个阶段——当任一参与者准备失败时,全局事务回滚;全部就绪才统一提交。这是一个可运行的教学示例。
  1. class Participant模拟分布式事务中的参与者节点,每个节点有独立的提交或回滚能力
  2. def prepare(self)对应 2PC 的准备阶段:执行本地事务、锁定资源、返回“是否可提交”
  3. fail_prob=0.2控制参与者准备失败的概率,用来观察“有节点失败时全局回滚”的行为
  4. def run_two_phase_commit(self)协调者主流程:收集所有参与者投票,全票通过才提交
  5. if not all(votes)任一节点准备失败,则对所有参与者执行 rollback,保证原子性
  6. random.seed(3)固定随机种子,让你每次运行看到相同的失败/成功模式,便于教学观察

常见误区

误区很多初学者认为两阶段提交是一种完美的强一致方案,只要用了 2PC,分布式事务就一定不会出问题。看到《数据密集型应用系统设计》里 2PC 的示意图,就以为它是分布式事务的“终极答案”。

正解2PC 在协调者正常运行、网络不分区时能保证原子性,但它有三个先天弱点:第一,协调者单点故障会导致参与者长时间阻塞(锁资源无法释放);第二,如果协调者在第二阶段宕机,部分参与者可能已提交、部分未提交,形成不一致;第三,网络分区时,参与者之间无法互通消息,2PC 会无限期阻塞。用年夜饭比喻:总厨师长突然失联,三位主厨举着锅铲干等,谁也不敢动。

为何常见:教科书通常只画 2PC 的正常时序图,很少强调协调者崩溃和网络分区的边界场景。初学者容易被“两阶段”的简洁性迷惑,忽略了分布式系统中最核心的故障模型。

误区看到 Saga 有“补偿”、TCC 有"Cancel",就认为两者等价,选哪个都行。实际上很多文章确实把两者混为一谈,导致不少开发者混淆。

正解两者有本质区别。Saga 的每个子事务都是直接提交的,不需要预占资源,中间状态对外可见;TCC 的 Try 阶段先锁定资源,确认后才真正变更,中间状态不可见(至少在事务语义上)。Saga 适合长流程、低竞争场景;TCC 适合高竞争、需要防超卖的场景。打个比方:Saga 是“先把菜端上桌,客人吃到了发现不对再撤下来换一道”;TCC 是“先跟客人确认菜单,预订好了食材,再开火——如果客人反悔,就把食材退回给供应商”。

为何常见:两者都属“补偿型事务”,概念上有重叠,初学者容易将它们归为一类。要区分它们,关键是抓住“资源是否预先锁定”这一核心差异。

误区不少开发者以为只要引入 MQ,把跨服务操作变成异步消息,就能保证最终一致性,不再需要其他兜底措施。他们把 MQ 的可靠性投递等同于业务的一致性保证。

正解MQ 只保证“消息能送达”(at least once),但不保证“消费者处理成功”、“处理结果与预期一致”。消费者代码可能异常、重复消费导致重复扣款、消息顺序错乱。最终一致性落地还需要三个配套机制:消费者幂等(通过业务 ID 去重)、失败重试(重试队列 + 死信队列)、定时对账(周期性比对数据,发现差异自动或人工补偿)。回到比喻:消息队列是“传话的跑腿小哥”,他保证你的话送到了,但对方是否听了、听了是否做对,需要你另外确认。

为何常见:MQ 产品的宣传语常常强调“高可用、高可靠”,让开发者产生“用了 MQ 就万事大吉”的错觉。实际上 MQ 只是传输管道,事务一致性的职责一直在应用层。

误区有些开发者在任何场景下都回避分布式事务,一律采用“异步消息 + 最终一致性”。他们认为 2PC/TCC 性能太差,不符合高并发要求,最终一致性是唯一正确选择。

正解选型必须依据业务约束,而不是一刀切。金融转账如果采用最终一致性,可能出现“扣款成功但入账延迟半小时”,这在合规上不可接受;库存扣减如果不用锁,秒杀场景必然超卖。2PC 的阻塞问题在低延迟网络和短事务下并没有那么可怕;TCC 的复杂编码也能通过框架(如 Seata、DTX)大幅降低。正确做法是:根据对一致性、可用性、吞吐的要求分场景选择,甚至在同一系统中混合使用不同方案——核心链路强一致,非核心链路最终一致。

为何常见:互联网公司的高并发故事容易让人形成“高性能 = 最终一致性”的刻板印象。实际上,不同业务模块的诉求差异极大,架构师的价值正在于做精细化选型。

误区初学者常以为 TCC 的 Cancel 就等同于数据库事务的 rollback——把 Try 阶段的操作“撤销”即可,没什么特别的技术含量。

正解数据库 rollback 是引擎层面的能力,它基于 undo 日志自动还原数据;而 TCC 的 Cancel 是业务代码,必须由开发者自己编写。它还必须是幂等的——由于网络重试,Cancel 可能被调用多次,每次执行结果必须一致。此外,Cancel 还可能涉及外部系统的操作(如调退款接口、释放第三方库存),比数据库 rollback 复杂得多。打个比方:rollback 是“把锅里的菜倒掉重来”,Cancel 是“把已经送出去的外卖追回来还给客户退款”——后者复杂得多。

为何常见:TCC 的方法命名(Try/Confirm/Cancel)容易让人联想到数据库事务的 begin/commit/rollback,但它们的实现层次完全不同,一个是业务层,一个是存储引擎层。

面试题

Q1. 两阶段提交(2PC)中,协调者在第一阶段向参与者发送什么指令?

单选题 · easy

  • A.Commit 指令,要求立即提交
  • B.Prepare 指令,要求执行并锁定资源但不提交← 正确答案
  • C.Abort 指令,要求立即回滚
  • D.Check 指令,要求检查网络状态

答案:B。2PC 第一阶段(准备阶段)由协调者向参与者发送 Prepare 请求,参与者执行事务操作并锁定资源,但暂不提交、返回“就绪”或“失败”。这就好比总厨师长先问各位主厨“能开始炒菜吗”,主厨们备好料但不点火。Commit 是第二阶段才发出的。

Q2. 下面哪一项不是 2PC 的已知缺点?

单选题 · easy

  • A.协调者单点故障会导致系统阻塞
  • B.参与者在准备阶段后可能长时间持有资源锁
  • C.无法处理所有参与者都是只读事务的场景← 正确答案
  • D.协调者故障时参与者无法自主决策

答案:C。2PC 的典型缺点是同步阻塞和协调者单点故障。如果所有参与者都是只读事务,2PC 依然正常运作——它还简化了流程。选项 A/B/D 都是 2PC 的真实缺陷,只有 C 不是。

Q3. 3PC 比 2PC 多出的关键机制是什么?

单选题 · mid

  • A.引入消息队列提高吞吐
  • B.在参与者中启用超时机制,允许超时后自行决策← 正确答案
  • C.使用分布式锁替代数据库锁
  • D.增加一个中心协调备份节点

答案:B。3PC 在 2PC 的基础上引入了“超时后自主决策”机制:参与者在等待协调者指令超时后,可根据自身状态选择提交或中止,避免无限阻塞。2PC 中参与者只能死等协调者。3PC 还增加了 CanCommit 预检,但核心改进是超时降级。

Q4. Saga 事务的两种执行模式是什么?(多选)

多选题 · mid

  • A.编排(Orchestration)← 正确答案
  • B.同步(Synchronous)
  • C.协同(Choreography)← 正确答案
  • D.强一致(Strong Consistency)

答案:AC。Saga 的两种模式是编排与协同。编排模式由中心协调器控制各子事务的执行与补偿;协同模式中,各服务通过事件完成串联,没有中心控制。同步和强一致都不属于 Saga 的模式划分。

Q5. TCC 模式中,Try 阶段的核心职责是什么?

单选题 · mid

  • A.直接执行最终业务变更并提交
  • B.检查业务可行性并预留资源,但不做最终变更← 正确答案
  • C.异步通知所有下游服务执行操作
  • D.将事务状态写入分布式日志中心

答案:B。TCC 的 Try 阶段执行两件事:业务检查(资源是否够)和资源预留(锁定),并不做最终的数据变更。真正的变更在 Confirm 阶段执行。这一设计让 TCC 能有效预防超卖等并发问题。

Q6. 关于 Saga 与 TCC 的区别,下列说法正确的是?(多选)

多选题 · hard

  • A.Saga 的中间状态对外可见,TCC 的资源在 Try 阶段被锁定,中间状态基本不可见← 正确答案
  • B.Saga 的每个子事务直接提交,TCC 需要三个方法协同← 正确答案
  • C.TCC 的编码复杂度通常高于 Saga← 正确答案
  • D.Saga 与 TCC 都需要依赖两阶段提交协议

答案:ABC。Saga 子事务提交后结果立即可见,TCC 通过 Try 锁定资源避免了中间状态暴露;Saga 只需正向+补偿两个方法,TCC 需要 Try/Confirm/Cancel 三个方法,更复杂;两者都是补偿型事务,但都不依赖 2PC 协议。选项 D 错误。

Q7. 本地消息表方案中,“业务操作与消息写入同一个数据库事务”的目的是什么?

单选题 · mid

  • A.提高数据库写入吞吐量
  • B.保证业务操作与消息记录要么同时成功、要么同时失败← 正确答案
  • C.简化消费者代码的幂等逻辑
  • D.避免使用消息队列中间件

答案:B。将业务操作和消息插入放在同一个本地事务中,是本地消息表方案的核心技巧。它借用了单机事务的原子性,从根本上防止“订单创建了但消息没发出去”的不一致。如果二者在不同事务中,就可能出现订单入库但消息未发送,导致下游漏处理。

Q8. 如下哪种场景最适合使用 TCC 模式?

单选题 · easy

  • A.用户浏览商品,记录访问日志
  • B.秒杀系统中扣减库存,防止超卖← 正确答案
  • C.管理员修改系统配置项
  • D.同步用户头像到 CDN

答案:B。秒杀系统的核心痛点是防止多个请求同时扣减同一件库存导致超卖。TCC 的 Try 阶段锁定资源,正好能解决这一高竞争场景。浏览日志、配置修改、CDN 同步都不需要复杂的一致性保证,用普通操作或异步消息即可。

Q9. 以下哪些措施是“最终一致性”落地的必要组成部分?(多选)

多选题 · hard

  • A.消费者幂等处理← 正确答案
  • B.失败重试机制← 正确答案
  • C.定时对账任务← 正确答案
  • D.数据库全局锁

答案:ABC。最终一致性方案的核心是可靠消息链路,它需要消费者幂等(去重)、失败重试(队列重投)和定时对账(兜底发现不一致)三者配合,才能做到“最终一致”。全局锁属于强一致方案的工具,与最终一致性方向相悖。

Q10. 协调者向参与者发送提交指令,但有参与者未收到指令,随后协调者宕机。该场景最可能出现什么问题?

单选题 · mid

  • A.所有参与者自动回滚,事务安全终止
  • B.部分参与者已提交,部分未提交,全局状态不一致← 正确答案
  • C.协调者重启后自动补偿,系统恢复一致
  • D.参与者之间互相通信,自动达成共识

答案:B。2PC 中协调者在提交阶段宕机,已经收到提交指令的参与者会提交事务,未收到的则阻塞等待,导致数据不一致。这正是 2PC 的经典故障场景。没有额外的恢复机制,协调者重启也不会自动补偿;参与者之间也不存在共识协议。这也是 3PC 尝试通过“超时自主决策”缓解的问题。