笃行
首页
个人 & 心法
互联网/硬件后台
游戏基础架构
UE 引擎
游戏业务
AI / 大模型
数据结构与算法
机器学习数学
通用基础
GitHub
首页
个人 & 心法
互联网/硬件后台
游戏基础架构
UE 引擎
游戏业务
AI / 大模型
数据结构与算法
机器学习数学
通用基础
GitHub
  • 通用后台基础(跨域)

    • 并发模型 · 进程 / 线程 / 协程
    • 操作系统核心与零拷贝
    • Go 语言基础与常见陷阱
    • C++11 语言基础与常见陷阱
    • C++20 语言基础与常见陷阱
    • Rust 语言基础与常见陷阱
    • 设计模型 · Actor / CSP / Reactor / 同步异步
    • GC 与 STW · Go / JVM
    • 可观测性
    • 时序异常检测(EWMA / ARIMA / 滑动窗口)
    • 数据库范式与存储引擎:从关系型到向量库
    • MySQL InnoDB 索引与事务
    • Redis 版本演进 & 分布式
    • 消息队列 · 可靠投递与选型
    • 分布式事务 · 2PC / TCC / Saga / 最终一致性
    • HTTP / HTTPS / TLS 与 RPC
    • 加密基础:对称 / 非对称 / 哈希与组合模式
    • 叙事主骨架轴选择方法论 SOP

分布式事务 · 2PC / TCC / Saga / 最终一致性

为什么单机事务失效 · CAP 与 BASE · 2PC/3PC · TCC 三段 · Saga 补偿 · 本地消息表 · 可靠消息最终一致性 · 最大努力通知 · 幂等与空回滚/悬挂

一句话抓手

分布式事务的一切方案都在回答一个问题:多个独立节点的写,如何做到"要么都成功、要么都不影响"。核心权衡是强一致(2PC,牺牲可用性和性能)vs 最终一致(TCC/Saga/消息,牺牲实时一致性换可用性和吞吐)。互联网后台绝大多数选最终一致,因为强一致的同步阻塞在高并发下扛不住。记住:"没有银弹,只有幂等 + 补偿/重试 + 对账兜底"。

场景问题

打个比方(2PC):两阶段提交就像婚礼上的牧师主持。第一阶段"问准备":牧师分别问新郎新娘"你愿意吗?"——双方都得先锁定状态、答一句"我愿意(prepare 成功、资源锁住)"。第二阶段"下决定":只有两边都说愿意,牧师才宣布"我宣布你们结为夫妻(commit)";只要有一个反悔,就当场喊停(rollback)。类比失效边界:问题就出在这两句话中间的空档——双方答完"我愿意"到牧师宣布之间,得傻站在台上一动不动(全程锁着资源);万一牧师这时晕倒了(协调者单点宕机),这俩人就永远僵在台上、进退不得(参与者阻塞卡死)。这正是 2PC 在高并发生产环境几乎不用的原因,也是大家转投 Saga(一连串"可吃后悔药"的补偿步骤)、TCC、可靠消息这些最终一致方案的动机。

后台面试与系统设计高频题,本质考"跨服务/跨库写的一致性代价":

题目考点直觉答案往往错在
下单扣库存跨了两个服务怎么保证一致分布式事务选型不能直接用本地事务;要 TCC/Saga/消息
2PC 为什么生产少用同步阻塞 + 协调者单点全程锁资源,协调者挂了参与者卡死
TCC 的 Try 阶段做什么资源预留预留而非真扣(冻结库存),不是直接扣减
Saga 回滚怎么做补偿而非回滚已提交的本地事务无法回滚,靠反向补偿
消息最终一致怎么保证消息不丢本地消息表业务和"记消息"必须同一本地事务原子
TCC 空回滚 / 悬挂是什么网络乱序Cancel 先于 Try 到达、Try 网络超时后又到
强一致一定比最终一致好吗权衡高并发下强一致的锁和阻塞常常不可接受

实现方案

为什么单机事务失效 + CAP/BASE 底座

单机 DB 的 ACID 靠一个事务管理器 + 一份 WAL/undo 保证。一旦写落在不同库/不同服务,没有统一的事务管理器,本地 commit 后无法跨节点回滚。

  • CAP:网络分区(P)不可避免,只能在一致性 C 和可用性 A 间取舍。分布式事务本质是在 CP 与 AP 之间选。
  • BASE:Basically Available(基本可用)+ Soft state(软状态/中间态)+ Eventually consistent(最终一致)。互联网系统的主流哲学——放弃实时强一致,换高可用与高吞吐,用一段时间后收敛到一致。

2PC / 3PC:强一致,同步阻塞

2PC(两阶段提交):协调者驱动,Prepare(各参与者执行但不提交、锁资源并回复能否提交)→ Commit/Abort(全 yes 才提交)。

2PC 的三宗罪

  • 同步阻塞:Prepare 到 Commit 之间所有参与者锁住资源等待,高并发下吞吐骤降
  • 协调者单点:协调者在 Commit 阶段宕机,参与者已 Prepare 但不知该提交还是回滚,资源永久卡死
  • 数据不一致:Commit 阶段部分参与者收到、部分没收到(网络分区),出现不一致

3PC 加了 CanCommit 预询问和超时机制缓解阻塞,但增加一轮通信、仍不能根治不一致。生产上 2PC 只用于 DB 内部(XA)等强一致小范围场景,跨服务几乎不用。

TCC:Try-Confirm-Cancel,业务层两阶段

把 2PC 上移到业务层,用资源预留代替资源锁定:

  • Try:预留资源(冻结库存、冻结金额),不是真正扣减
  • Confirm:确认执行(把冻结转为实扣),必须幂等
  • Cancel:释放预留(解冻),必须幂等

TCC 三大坑:空回滚、悬挂、幂等

  • 空回滚:Try 因网络超时没执行,但事务管理器发起了 Cancel → Cancel 要能识别"没 Try 过"并直接返回成功(记录事务状态判断)
  • 悬挂:Try 超时后 Cancel 先到并完成,随后迟到的 Try 才到 → 资源被预留后再无人 Confirm/Cancel → Try 要检测"已 Cancel 过则拒绝执行"
  • 幂等:Confirm/Cancel 会重试,必须幂等

TCC 一致性强、无长时间锁,但每个服务要实现三个接口,侵入性大、开发成本高。

Saga:长事务的补偿链

把一个长事务拆成一串本地事务 T1…Tn,每个 Ti 配一个补偿 Ci。正常顺序执行 T1→Tn;中途 Ti 失败则反向执行 Ci-1→C1 补偿已完成的步骤。

正向 T1→T4 顺序执行;若 T3 扣款失败,则反向补偿已完成的 T2、T1(C2 补库存 → C1 取消订单),已提交的本地事务无法回滚只能语义补偿。

  • 编排式(Orchestration):中央协调器按流程调用各步骤和补偿(清晰、易监控)
  • 协同式(Choreography):各服务通过事件互相触发(去中心,但流程分散难追踪)

Saga 是"补偿"不是"回滚"

Ti 一旦本地提交就对外可见(无隔离性),失败时只能用 Ci 做语义补偿(如"已发货"不能撤销,只能补一个退货流程)。所以 Saga 不保证隔离性,可能读到中间态;且补偿本身也可能失败,要重试 + 兜底。适合流程长、可补偿、能容忍中间态的业务(订单、履约)。

可靠消息最终一致性(最常用)

用 MQ 把"跨服务写"异步化,核心是保证"本地事务成功 ⟺ 消息一定发出":

  • 本地消息表:业务更新 + 插入"待发消息"记录在同一个本地事务里原子提交 → 后台轮询消息表投递 MQ → 下游消费(幂等)→ 投递成功标记/删除。DB 事务保证"改了库就一定记下消息",MQ 重投 + 消费幂等保证最终送达
  • 事务消息(RocketMQ):发半消息 → 执行本地事务 → 提交/回滚半消息 → Broker 定时回查本地事务状态兜底(解决"本地事务成功但确认消息丢失")

下游必须幂等消费(见 消息队列 与 幂等设计)。

最大努力通知

发起方尽最大努力(多次重试、退避)通知接收方,接收方需提供查询接口让发起方对账。用于对最终一致性要求不高、且接收方可能不可靠的场景(支付结果通知第三方、开放平台回调)。

对账兜底

任何最终一致方案都应有离线对账:定时扫描两边数据,发现不一致(如库存冻结了但订单没成)就补偿或告警。这是所有异步方案的最后一道防线。

为什么这么做

  • 为什么互联网偏好最终一致:高并发场景下,2PC 的同步锁定会让吞吐塌方、可用性受协调者单点拖累。BASE 用"短暂不一致 + 最终收敛"换来高可用和高吞吐,符合大多数业务"能接受几秒内看到中间态"的现实。
  • 为什么 TCC 用预留而非直接操作:Try 阶段冻结资源既避免了 2PC 的长时间数据库锁(冻结是业务状态,不锁行),又保留了 Confirm 前可 Cancel 的两阶段能力——把 2PC 的锁粒度从 DB 行降到业务资源。
  • 为什么本地消息表能防丢:把"发消息"这个动作转化成"往本地库写一行",从而纳入本地 ACID 事务。业务和消息记录同生共死,再靠异步投递 + 重试保证送达——用一次本地事务的原子性撬动跨服务的最终一致。

为什么别的选择不行

  • 为什么不全用 2PC 图省事:同步阻塞 + 协调者单点 + 分区不一致三个硬伤,让它在高并发跨服务场景不可用。它把可用性押给了协调者,与互联网"高可用优先"背道而驰。
  • 为什么不无脑上 TCC:每个参与服务都要额外实现 Try/Confirm/Cancel 三接口并处理空回滚/悬挂/幂等,开发和维护成本高。只有对一致性要求高、且能改造下游的核心链路(资金、库存)才值得。
  • 为什么不能只靠重试不做幂等:重试是最终一致的基石,但重试必然带来重复执行,没有幂等就会重复扣款/重复发货。幂等和重试是一对,缺一不可。

沉淀结论

面试速答清单:

  • 单机 ACID 跨节点失效;CAP 分区下选 CP/AP,互联网主流走 BASE 最终一致
  • 2PC 强一致但同步阻塞 + 协调者单点 + 可能不一致,跨服务几乎不用
  • TCC = 业务层两阶段:Try 预留 / Confirm 实扣 / Cancel 解冻;须处理空回滚、悬挂、幂等
  • Saga = 本地事务链 + 反向补偿;无隔离性,读得到中间态,适合长流程可补偿业务
  • 可靠消息最终一致最常用:本地消息表 / RocketMQ 事务消息 + 回查;下游幂等消费
  • 兜底三件套:幂等 + 重试(退避) + 离线对账

选型速记

方案一致性侵入性吞吐适用
2PC/XA强低(DB 支持)低DB 内部、强一致小范围
TCC强(准实时)高(三接口)高资金/库存等核心链路
Saga最终中(写补偿)高长流程、可补偿(订单履约)
可靠消息最终中高异步解耦、跨服务通知
最大努力通知最终(弱)低高对外回调、容忍不达

记忆口诀

  • 底座:CAP 分区必选 / CP 强一致 vs AP 高可用 / BASE 最终一致
  • 2PC:同步阻塞 / 协调者单点 / 分区不一致(跨服务不用)
  • TCC:Try 预留 / Confirm 实扣 / Cancel 解冻(防空回滚·悬挂·须幂等)
  • 兜底:幂等 / 重试退避 / 离线对账

相关专题:游戏跨渠道限购(同一件限购在游戏内 + 官网 + iOS 米大师 + 安卓渠道多入口售卖)的落地事务链——TCC 预扣 + 本地消息表 + 离线对账——详见 跨渠道限购与游戏侧分布式事务落地。

内容来源

关键点整理自《Designing Data-Intensive Applications》(Martin Kleppmann,第 7/9 章)、Seata 官方文档(AT/TCC/Saga 模型)、RocketMQ 事务消息设计与 CAP/BASE 经典论述重写为五段式。请以官方文档为准。

自测:合上资料能说清楚吗?

  1. 为什么单机数据库的 ACID 事务一旦写落到不同库/不同服务就失效了?分布式事务本质在解决什么问题?
  2. 2PC 生产上跨服务几乎不用,它到底有哪三个硬伤?
  3. TCC 的空回滚和悬挂分别是什么场景造成的?各自怎么防?
  4. 「本地消息表」凭什么能保证「本地事务成功 ⟺ 消息一定发出」?
  5. 对比 TCC 与 Saga:一致性、隔离性、侵入性、适用场景各有什么不同?
参考答案
  1. 无统一事务管理器与共享 WAL/undo,本地 commit 后无法跨节点回滚。本质:多节点写做到要么都成功、要么都不影响。

  2. 同步阻塞(Prepare→Commit 全程锁资源)、协调者单点(宕机则参与者卡死)、分区不一致(部分收到 Commit)。

  3. 空回滚:Try 因超时没执行却收到 Cancel → Cancel 靠事务状态识别「没 Try 过」直接成功。悬挂:Cancel 先到完成后 Try 迟到 → Try 检测「已 Cancel 则拒绝」。

  4. 业务更新与「插入待发消息」在同一本地事务原子提交,改库就必记消息;再靠轮询投递 + MQ 重投 + 消费幂等保证最终送达。

  5. TCC:准实时强一致、无隔离问题、侵入高(三接口),用于资金/库存核心链路。Saga:最终一致、无隔离性(读得到中间态)、侵入中(写补偿),用于长流程可补偿业务。

最近更新: 2026/9/10 11:38
Prev
消息队列 · 可靠投递与选型
Next
HTTP / HTTPS / TLS 与 RPC