分布式每日一学 — Day 4:分布式事务(2PC / 3PC / Saga / TCC)

📅 2026/8/11 12:59:43
分布式每日一学 — Day 4:分布式事务(2PC / 3PC / Saga / TCC)
分布式每日一学 — Day 4分布式事务2PC / 3PC / Saga / TCC前 3 天我们打的是「副本一致性」这条线CAP 告诉我们 P 没得选Raft/Paxos 告诉我们怎么让一组机器对同一份数据达成一致。今天我们换一个战场——「跨多个服务/数据库做完一整件事」怎么保证不出错。一个下单链路要改订单库、库存库、账户库、积分库任何一个环节挂了都不能让用户的钱消失或凭空多出来。这就是分布式事务的领域。 昨日思考题解答Day 3 — Paxos题目回顾5 节点 Paxos 集群 A/B/C/D/E。P1在 A 上发起Prepare(1)A/B/C 回 Promise 且未接受过提案 → P1 准备Accept(1, V1100)时 A 宕机重启 → P2在 D 上发起Prepare(2)B/C/D/E 回 Promise → B 报告收到过 Accept(1, V1100) 但还没回复就被打断。Q1P2 应该选什么 V 提交答案P2 必须选 V 100。推导过程B 是否真正「接受」了 Accept(1, V1100)Paxos 中接受accept指的是 Acceptor 收到 Accept 请求后、检查编号 ≥ 自己承诺的最小编号 → 记录该提案。题目说 B “收到过 Accept 但还没回复”——只要 B 已经记录了(1, V1100)就视为已接受无论 ACK 是否发出去。B 在 Promise(2) 时报告了什么Acceptor 的 Promise 必须附带自己已接受的最高编号提案。所以 B 在回复 P2 的Prepare(2)时会报告已接受 (1, V1100)。Paxos Phase 2 的值选择规则Proposer 收到多数 Promise 后如果有 Acceptor 报告已接受提案 → 选编号最大的那个已接受值如果没人报告 → 用自己的值这里 B 报告了(1, V1100)所以P2 即使本来想提别的值也必须改成 V100。这是 Paxos 安全性Safety的核心一旦某个值被多数 Acceptor 接受或即将被接受后续 Proposer 就无法再改变它。哪怕 P2 是后来者也必须服从已有的决议。Q2后续如果 P1 醒来再发起 Accept(1, V1) 会发生什么答案Accept(1) 会被拒绝P1 必须用更高编号重新发起。推导过程时间线回顾 P1 的 Accept(1) 发出时 A 宕机了 之后 B/C 已经 Promise(2) → 承诺不再接受编号 2 的提案 P1 醒来后发 Accept(1, V1100) ┌─────────────────────────────────────────────┐ │ B: 拒绝 ❌已 Promise(2)1 2 │ │ C: 拒绝 ❌已 Promise(2)1 2 │ │ D: 拒绝 ❌已 Promise(2)1 2 │ │ E: 拒绝 ❌已 Promise(2)1 2 │ │ A: 可能接受 ✅重启后磁盘保留 Promise(1) │ └─────────────────────────────────────────────┘ P1 最多只能拿到 A 的 1 票 → 不够多数 → Accept 失败P1 的出路必须用更高编号如Prepare(3)重新发起提案。但即使 P1 用编号 3 重新跑Phase 1 时 B/C/D/E 中只要有人报告已接受 (2, V100)P1 就必须继续选 V100。结论值 100 已经锁定在系统中任何后续提案都无法改变它。这就是 Paxos 保证的“一旦值被选定就永远不变”。关键启示Paxos 规则本题体现Acceptor Promise 后不再接受更低编号提案B/C 拒绝 P1 的 Accept(1)Proposer 必须采用已接受的最高编号值P2 被迫选 V100安全性优先于活性Safety Liveness值锁定后不可逆但 P1 可能需要重试活锁风险一、为什么需要分布式事务单机 ACID 不够用了吗我们先复习一下单库事务的四大法宝ACID属性含义A — Atomicity原子性要么全做、要么全不做C — Consistency一致性事务前后数据满足约束I — Isolation隔离性并发事务互不干扰D — Durability持久性提交后数据不丢这套机制在一个数据库内部很完美。但只要业务开始拆开——分库分表、微服务化、跨库 Join、跨服务调用——单库 ACID 就罩不住了。 一个真实例子电商下单用户下单链路里要碰 5 个东西 1. 订单服务 → 写订单表 (order_db) 2. 库存服务 → 扣库存 (stock_db) 3. 账户服务 → 扣余额 (pay_db) 4. 积分服务 → 加积分 (point_db) 5. 消息服务 → 发短信 (msg_service)如果做到第 3 步账户服务挂了、库存已经扣了订单也已经写了——用户的钱付了但货没下成平台要背锅。 这就是分布式事务要解决的核心问题让一组跨资源/跨服务的写操作要么全部成功要么全部看起来没发生过。二、方案 1两阶段提交2PCTwo-Phase Commit这是最古老、最直接的思路来自于 1970 年代的数据库理论。 角色角色职责协调者Coordinator老大哥负责问所有人准备好了没和最后拍板提交/回滚参与者Participants各个数据库/服务执行具体写操作 两个阶段协调者 参与者们 │ │ ┌────────────┼────────────┐ │ │ ▼ │ │ ┌───┴───┐ ┌───┴───┐ ┌───┴───┐ │ │ 库存DB │ │ 订单DB │ │ 账户DB │ │ └───┬───┘ └───┬───┘ └───┬───┘ │ │ │ │ │ └────────────┼────────────┘ │ │ │ ═══════════════════════════════════════════════════ 阶段 1 ── Prepare准备 ═══════════════════════════════════════════════════ │ ▼ 协调者问所有人: 可以提交吗 ┌────────────┼────────────┐ │ │ │ ▼ ▼ ▼ 库存 订单 账户 上锁写undo 上锁写undo 上锁写undo 回 Yes 回 Yes 回 No❌ │ ═══════════════════════════════════════════════════ 阶段 2 ── Commit / Rollback根据投票结果拍板 ═══════════════════════════════════════════════════ │ 全 Yes → 协调者发 Commit → 各方真正写入 有 No → 协调者发 Rollback → 各方回滚到 undo 2PC 的四大致命问题问题解释同步阻塞整段时间内所有参与者都持有锁和资源其他请求被卡死协调者单点协调者宕机 整批参与者永远等第二阶段指令被锁死数据不一致协调者发完 Commit 后自己宕机部分参与者收到、部分没收到 → 状态分裂不幂等网络重发同一个 Commit可能触发重复写想想为什么 2PC 看起来很像 Raft/Paxos却没那么强因为 Raft/Paxos 解决的是同一份数据谁说了算而 2PC 是多个独立资源要不要一起动。它是投票没错但承诺是重的——一旦 Prepare 成功就锁住资源没有 Paxos 那样的编号单调性兜底。三、方案 2三阶段提交3PC理论界觉得 2PC 太脆于是加了阶段Phase 1 ── CanCommit 协调者问大家能提交吗不锁资源 Phase 2 ── PreCommit 参与者写 undo log回复 ACK正式待命 Phase 3 ── DoCommit 协调者发 commit参与者提交 3PC 相对 2PC 的改进多了一次预问减少盲目锁资源参与者超时机制超时未收到 DoCommit 就默认提交避免被锁死引入超时心跳之后协调者宕机参与者也能自己推下去 为什么工程上几乎不用 3PC原因说明协调者宕机还是无解3PC 的超时自提交在网络分区下反而会导致脑裂——一部分人 commit、一部分 rollback多一轮 RTT 太贵性能严重下降实现复杂度↑边界条件更多 业界真实状态3PC 是个教学价值高、工程价值低的方案。MySQL、Oracle、PG 的 XA 实现都是 2PC 系。四、方案 3Saga 模式微服务时代的宠儿1987 年由 Hector Garcia-Molina 提出被 Saga Service、Apache ServiceComb Seata 等广泛使用。 核心思想长事务拆成 T 补偿 C把一笔大事务拆成若干子事务每个子事务有自己的反向补偿操作主流程 T1 → T2 → T3 → T4 → ✅ 补偿流程 ← C1 ← C2 ← C3 ← C4 ↑ T1 失败 → 走 C2.5 补偿 T2再走 C1 补偿 T1例如订餐链路的 Saga 拆解子事务正向 T补偿 C订单创建T1: 写入订单C1: 撤销订单库存扣减T2: 减库存数C2: 加回库存扣款T3: 账户减余额C3: 加回余额发短信T4: 通知用户C4: 发撤回短信 两种 Saga 协调方式方式 A编曲式 / Choreography事件驱动每个 T 完成就发 MQ 事件下游订阅没有中心协调器优点解耦、简单缺点链路难追踪、容易循环依赖方式 B编排式 / Orchestration中心化有一个 Saga Coordinator按顺序调用 T1→T2→…类似工作流引擎优点链路可视化、易调试缺点协调器成了关键路径⚠️ Saga 的几个硬骨头隔离性弱T2 已扣库存但 T3 失败时库存被别人看到已经少了但最终要补回去——中间窗口期是脏读。必须幂等补偿可能被重试每个 Ti / Ci 都要能经得起重复执行。补偿不一定能完全反向比如发短信发出去没法撤回只能再发一条说明。这是补偿 ≠ 反向事务的关键区别。五、方案 4TCCTry-Confirm-Cancel支付宝的蚂蚁金服、阿里 Seata 主推的模式比 Saga 早但工程侵入性更强。 三阶段阶段含义关键Try资源预留扣库存时只把可用 → 冻结不真正减必须可逆Confirm真正提交冻结 → 扣减全部 Try 成功后调用必须幂等Cancel释放预留把冻结 → 可用还回去必须幂等所有 Try 全部成功 → 批量 Confirm这一步通常异步化 任一 Try 失败 → 调用已成功的 Try 对应的 Cancel Saga vs TCC维度SagaTCC一致性强度最终一致接近强一致业务侵入性低只加补偿方法高每个业务要写 Try/Confirm/Cancel 三套资源占用不锁资源Try 阶段长期占着冻结额性能好一般多一次 Reserve适用场景长链路 跨多服务短链路 对一致性要求高支付类 选型口诀链长、轻业务 → Saga链短、重资金 → TCC。六、其他常见野路子方案实战高频除了上面四个学院派工程上更常用的是消息中间件派的最终一致方案 本地消息表经典老炮主流程 异步 ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ 业务表写入 │ │ 消息表写入 │ ──轮询──│ 投递到 MQ │ └──────────┘ └──────────┘ └──────────────┘ └─ 同一事务一致 ✔ ↓ 下游消费 MQ处理业务 失败 → 重试 N 次 → 人工介入关键点业务表和消息表写在同一个本地事务靠后台 polling 把消息从 MySQL 推到 MQ。简单、好理解、几乎人人用过。 事务消息RocketMQ 5.0 一类生产者发送 Half 消息到 MQMQ 不投递本地事务完成 → 二阶段回调告诉 MQ 是 commit / rollback回查机制MQ 定期问生产者刚才那个消息你还活着吗避免悬挂 最大努力通知跨公司用不要求强一致只要求尽量通知到典型场景第三方支付回调、退款通知、运营商充值回调配重试 幂等 对账补偿七、大对照表建议截图保存方案一致性性能可用性业务侵入复杂度典型场景2PCXA强一致❌ 差❌ 协调者单点低中单库多库、强一致短事务3PC强一致❌ 很差⚠️中高几乎不用Saga最终一致✅ 好✅ 高中写补偿中长链路、跨多服务、订单TCC接近强一致⚠️ 一般✅ 高高三套方法高支付、资金类短链本地消息表最终一致✅ 好✅ 高低低99% 的常规业务异步解耦事务消息最终一致✅ 好✅ 高中中不希望有 polling 的升级版最大努力通知弱一致✅ 好✅ 高极低低跨公司回调、第三方通知八、实战选型决策树Q1: 是不是要求强一致 数据量不大 ├─ 是 → 2PC (XA)例如 Seata-XA └─ 否 ↓ Q2: 链路上每个动作都能很方便定义补偿吗 ├─ 不能 → TCC强制业务做预留 └─ 能 ↓ Q3: 链路长不长3 个子事务 ├─ 长 → Saga编排式用 Camunda/Cadence/Seata Saga └─ 短 → Saga 或 本地消息表 都行 Q4: 跨公司吗 └─ 是 → 最大努力通知 对账 幂等实战真相80% 的分布式事务场景用「本地消息表 MQ」就能搞定剩下 18% 用 Saga最后 2% 才是 TCC/2PC 战场。别一上来就上 TCC复杂度的代价远超想象。九、和前 3 天的连接之前的知识点和分布式事务的关系CAP 理论Day 1分布式事务本质是在 P 必然存在下选 C 或 A2PC 偏 CSaga 偏 ARaftDay 2共识算法是 2PC / Saga 协调器的心跳保活基础PaxosDay 3Seata-Server 集群内部就用了类 RAFT/Paxos 来保持协调器高可用也就是说分布式事务 业务一致性协议 共识协议 MQ。三块拼起来才是一套完整方案。十、课堂思考题 想象一条真实的转账链路用户 A 给用户 B 转账 100 元要走① 风控服务检查是不是洗钱→② 账户服务A -100B 100→③ 账本服务写流水→④ 积分服务A 10 积分→⑤ 通知服务短信/站内信。问题 1你会用 Saga 还是 TCC为什么问题 2如果用 Saga② “A -100、B 100” 这个子事务补偿动作具体怎么设计要注意什么边界条件问题 3如果 MQ 通知用户短信发送失败重试到第 3 次还是失败你怎么办这条业务流算成功还是失败提示风控/账本可重账户主操作要幂等积分是福利通知是尽最大努力。一句话带走CAP 告诉你分布式是有代价的Raft/Paxos 告诉你副本怎么共识而分布式事务告诉你怎么把跨服务的多个写操作缝合成一件要么全成要么全不成的事。80% 的场景用本地消息表 MQ 就够了剩下 20% 才是 Saga 和 TCC 的战场。明天预告Day 5 — 分布式锁Redlock、Redisson、ZooKeeper 锁、Chubby 锁以及为什么我说你不能完全相信 Redis 的 SETNX