分布式事务完全演进链:从单体事务到 TCC 、Saga 与最终一致性
- 第一部分:单体事务的底层机制彻底理解
-
- [1. 什么是事务(Transaction)](#1. 什么是事务(Transaction))
-
- [1.1 一句话定义](#1.1 一句话定义)
- [1.2 事务真正依赖的不是"运气",而是 ACID](#1.2 事务真正依赖的不是“运气”,而是 ACID)
- [1.3 一个贯穿全文的例子:银行转账](#1.3 一个贯穿全文的例子:银行转账)
- [2. 单体事务的底层机制(数据库到底做了什么)](#2. 单体事务的底层机制(数据库到底做了什么))
-
- [2.1 日志机制:Undo log / Redo log](#2.1 日志机制:Undo log / Redo log)
- [2.2 锁机制:事务隔离的真正来源](#2.2 锁机制:事务隔离的真正来源)
- [2.3 隔离级别:数据库如何在"正确"和"性能"之间取舍](#2.3 隔离级别:数据库如何在“正确”和“性能”之间取舍)
- [2.4 缓冲区与提交机制:为什么事务不慢](#2.4 缓冲区与提交机制:为什么事务不慢)
- [2.5 回滚机制:事务失败时,系统如何自救](#2.5 回滚机制:事务失败时,系统如何自救)
- [3. 为什么单体事务无法跨库(这是分布式事务的起点)](#3. 为什么单体事务无法跨库(这是分布式事务的起点))
- [4. 本部分你必须牢牢记住的几件事](#4. 本部分你必须牢牢记住的几件事)
- [第二部分:分布式事务的本质问题与 2PC 协议](#第二部分:分布式事务的本质问题与 2PC 协议)
-
- [1. 分布式事务到底难在哪里](#1. 分布式事务到底难在哪里)
-
- [1.1 单体事务 vs 分布式事务:难度差异不是一个量级](#1.1 单体事务 vs 分布式事务:难度差异不是一个量级)
- [1.2 一个典型失败场景](#1.2 一个典型失败场景)
- [1.3 分布式事务失败的四个根因(非常重要)](#1.3 分布式事务失败的四个根因(非常重要))
- [2. 2PC:第一个系统级解决方案](#2. 2PC:第一个系统级解决方案)
-
- [2.1 2PC 解决的不是"提交",而是"一致决策"](#2.1 2PC 解决的不是“提交”,而是“一致决策”)
- [2.2 角色模型(必须记住)](#2.2 角色模型(必须记住))
- [3. 两阶段提交的完整流程(拆到原子级)](#3. 两阶段提交的完整流程(拆到原子级))
-
- 阶段一:Prepare(投票阶段)
- [阶段二:Commit / Rollback(决策阶段)](#阶段二:Commit / Rollback(决策阶段))
- [4. 2PC 的致命问题](#4. 2PC 的致命问题)
-
- [4.1 为什么 2PC 是"阻塞型协议"](#4.1 为什么 2PC 是“阻塞型协议”)
- [4.2 CAP 角度再看 2PC](#4.2 CAP 角度再看 2PC)
- [5. 2PC 的优缺点](#5. 2PC 的优缺点)
- [6. 为什么 2PC 没有被广泛使用?](#6. 为什么 2PC 没有被广泛使用?)
-
- [6.1 2PC 的三个"理想前提"](#6.1 2PC 的三个“理想前提”)
- [6.2 2PC 的"致命工程缺陷"](#6.2 2PC 的“致命工程缺陷”)
- [6.3 工业界的选择:放弃"强一致",拥抱"可用性"](#6.3 工业界的选择:放弃“强一致”,拥抱“可用性”)
- [6.4 一句话总结](#6.4 一句话总结)
- 总结
- [第三部分:TCC ------ 把事务控制权从数据库交还给业务](#第三部分:TCC —— 把事务控制权从数据库交还给业务)
-
- [1. TCC 出现之前的核心问题](#1. TCC 出现之前的核心问题)
- [2. TCC 核心思想:事务不是一瞬间完成的](#2. TCC 核心思想:事务不是一瞬间完成的)
- [3. 完整库存场景理解 TCC](#3. 完整库存场景理解 TCC)
-
- [3.1 错误直觉](#3.1 错误直觉)
- [3.2 正确 TCC 库存模型](#3.2 正确 TCC 库存模型)
- [4. Try 阶段到底改没改数据库?](#4. Try 阶段到底改没改数据库?)
- [5. 读旧数据问题解析](#5. 读旧数据问题解析)
-
- [5.1 事务参与者](#5.1 事务参与者)
- [5.2 用户查询接口](#5.2 用户查询接口)
- [6. TCC 的"锁"在哪里?](#6. TCC 的“锁”在哪里?)
- [7. Confirm 与 Cancel:事务真正结局](#7. Confirm 与 Cancel:事务真正结局)
-
- [Confirm 阶段](#Confirm 阶段)
- [Cancel 阶段](#Cancel 阶段)
- [8. TCC 的三个隐藏难点](#8. TCC 的三个隐藏难点)
- [9. Try 成功但 Confirm 不来怎么办?](#9. Try 成功但 Confirm 不来怎么办?)
- [10. TCC 的适用场景与限制](#10. TCC 的适用场景与限制)
- [11. 核心记忆总结](#11. 核心记忆总结)
- [第四部分:Saga ------ 接受不一致,用时间换系统存活](#第四部分:Saga —— 接受不一致,用时间换系统存活)
-
- [1. Saga 出现的根本原因:长事务 TCC 承受不了](#1. Saga 出现的根本原因:长事务 TCC 承受不了)
- [2. Saga 的核心思想:事务不是"一次性提交"](#2. Saga 的核心思想:事务不是“一次性提交”)
- [3. Saga 的基本模型](#3. Saga 的基本模型)
- [4. Saga 与 TCC 的本质对比](#4. Saga 与 TCC 的本质对比)
- [5. Saga 的两种实现方式](#5. Saga 的两种实现方式)
-
- [编排式 Saga(Orchestration)](#编排式 Saga(Orchestration))
- [事件驱动 Saga(Choreography)](#事件驱动 Saga(Choreography))
- [6. Saga 的关键工程难点](#6. Saga 的关键工程难点)
- [7. Saga 的适用场景](#7. Saga 的适用场景)
- [8. 工程实践技巧](#8. 工程实践技巧)
- [9. 总结与记忆点](#9. 总结与记忆点)
- [第五部分:可靠消息 / 事务消息 ------ 把一致性交给时间](#第五部分:可靠消息 / 事务消息 —— 把一致性交给时间)
-
- [1. 先抛结论:为什么消息几乎是最终答案?](#1. 先抛结论:为什么消息几乎是最终答案?)
- [2. 一个最真实的业务问题(几乎人人都踩过)](#2. 一个最真实的业务问题(几乎人人都踩过))
- [3. 为什么"数据库事务 + 发消息"一定会失败?](#3. 为什么“数据库事务 + 发消息”一定会失败?)
- [4. 可靠消息的核心模式:本地事务 + 消息表(关键转折)](#4. 可靠消息的核心模式:本地事务 + 消息表(关键转折))
-
- [1️⃣ 核心思想(一句话)](#1️⃣ 核心思想(一句话))
- [2️⃣ 表结构示意(极简但够用)](#2️⃣ 表结构示意(极简但够用))
- [3️⃣ 正确的执行流程(必须理解透)](#3️⃣ 正确的执行流程(必须理解透))
-
- [Step 1:本地事务(原子保证)](#Step 1:本地事务(原子保证))
- [Step 2:异步投递(解耦关键)](#Step 2:异步投递(解耦关键))
- 本节记忆总结
- [5. 这套模型为什么"天然可靠"?](#5. 这套模型为什么“天然可靠”?)
-
- [场景 1:事务提交失败](#场景 1:事务提交失败)
- [场景 2:事务成功,但服务宕机](#场景 2:事务成功,但服务宕机)
- [场景 3:消息发出,但 ACK 丢失](#场景 3:消息发出,但 ACK 丢失)
- 本节记忆总结
- [6. 那"消息重复"怎么办?(必须吃透)](#6. 那“消息重复”怎么办?(必须吃透))
- [7. 事务消息(以 RabbitMQ 思想为例)](#7. 事务消息(以 RabbitMQ 思想为例))
- [8. 可靠消息 vs TCC vs Saga(最终对照)](#8. 可靠消息 vs TCC vs Saga(最终对照))
- [9. 可靠消息的隐性成本](#9. 可靠消息的隐性成本)
-
- [1️⃣ 延迟](#1️⃣ 延迟)
- [2️⃣ 状态复杂度](#2️⃣ 状态复杂度)
- [3️⃣ 运维成本](#3️⃣ 运维成本)
- 本节记忆总结
- 第六部分:如何在真实系统中选择分布式事务方案
-
- [1. 先给你一个残酷但真实的结论](#1. 先给你一个残酷但真实的结论)
- [2. 为什么"单体事务"能解决的问题,千万别拆?](#2. 为什么“单体事务”能解决的问题,千万别拆?)
- [3. 什么时候你"被迫"进入分布式事务?](#3. 什么时候你“被迫”进入分布式事务?)
- [4. 一套真实可用的"选择决策树"**](#4. 一套真实可用的“选择决策树”**)
-
- 第一步:能不能"异步最终一致"?
- [第二步:是否涉及"钱 / 库存 / 额度"?**](#第二步:是否涉及“钱 / 库存 / 额度”?**)
- 第三步:能不能冻结资源?
- [5. 一个完整的现实系统是怎么混用的?**](#5. 一个完整的现实系统是怎么混用的?**)
- [6. 最常见、最致命的 5 个误区**](#6. 最常见、最致命的 5 个误区**)
-
- [❌ 误区 1:追求"全局强一致"](#❌ 误区 1:追求“全局强一致”)
- [❌ 误区 2:到处用 TCC](#❌ 误区 2:到处用 TCC)
- [❌ 误区 3:把 MQ 当数据库](#❌ 误区 3:把 MQ 当数据库)
- [❌ 误区 4:忽略幂等和重试](#❌ 误区 4:忽略幂等和重试)
- [❌ 误区 5:设计时不考虑失败](#❌ 误区 5:设计时不考虑失败)
- [7. 你必须形成的三种"工程直觉"](#7. 你必须形成的三种“工程直觉”)
-
- 所有远程调用都可能失败
- 所有一致性都是"设计出来的"
- [数据不一致 ≠ 系统失败](#数据不一致 ≠ 系统失败)
- [8. 如果让你"从零设计一个系统",该怎么做?](#8. 如果让你“从零设计一个系统”,该怎么做?)
- [9. 你现在应该真正"吃透"的分布式事务观](#9. 你现在应该真正“吃透”的分布式事务观)
分布式事务,是现代微服务架构中最核心、最复杂、也最容易被误解的问题之一。
它难,不是因为概念多,而是因为:
-
每一种方案,都不是"完美解"
-
每一次演进,都是在为上一代方案还债
-
每一个结论,背后都是工程妥协的结果
要真正理解分布式事务,必须从单体事务开始,一路看清:
为什么单体事务可以"理所当然地正确" 为什么一拆服务,正确性立刻崩塌 为什么方案会从 2PC → TCC → Saga →
消息一致性一路演进
本文的目标不是"记住方案",而是:
👉 理解:每一种方案为什么会出现,又为什么不可能成为终点

第一部分:单体事务的底层机制彻底理解
在理解任何分布式事务方案之前,有一件事是绝对绕不过去的:
❗分布式事务之所以复杂,不是因为"分布式",而是因为:单体事务本身就已经很复杂。
如果你没有真正吃透单体事务的底层机制,那么后面所有方案------
2PC、TCC、Saga、消息一致性------都会变成"记结论,而不是理解原理"。
这一部分的目标只有一个:
👉 把数据库已经默默替你做了十几年的事,看清楚、想明白。
1. 什么是事务(Transaction)
1.1 一句话定义
事务,是一组操作的集合:
要么全部成功,要么全部失败,
不允许"做了一半"的中间状态被外界看到。
这是一个非常"反直觉"的能力,因为在现实世界中:
-
程序会崩溃
-
机器会断电
-
网络会中断
但数据库却承诺:即使发生上述任意情况,也不会留下"半成品"。
1.2 事务真正依赖的不是"运气",而是 ACID
事务之所以可信,靠的不是"代码写得好",而是一整套底层机制设计,这就是 ACID。
| 特性 | 表面含义 | 真正解决的问题 | 底层依赖机制 |
|---|---|---|---|
| 原子性(Atomicity) | 要么全做,要么全不做 | 防止"做了一半" | Undo log |
| 一致性(Consistency) | 数据始终合法 | 防止脏数据 | 约束 / 外键 / 业务 |
| 隔离性(Isolation) | 互不干扰 | 防止并发错乱 | 锁 + MVCC |
| 持久性(Durability) | 提交后不丢 | 防止宕机丢数据 | Redo log + 刷盘 |
⚠️ 非常重要的一点:
ACID 不是四个"口号",
而是四类非常具体、非常工程化的实现手段。
1.3 一个贯穿全文的例子:银行转账
A 给 B 转账 100 元
拆开来看只有两步:
-
A 账户余额 -100
-
B 账户余额 +100
但真正的问题是:
❓如果第 1 步成功,第 2 步失败,怎么办?
事务存在的意义,就是让系统回答:
👉 "我可以保证:世界上永远不存在这种状态。"
2. 单体事务的底层机制(数据库到底做了什么)
这一节非常关键,因为你后面会反复看到:分布式事务方案,本质上都是在"模拟"或"替代"下面这些机制
2.1 日志机制:Undo log / Redo log
先给结论(一定要记住)
🔥 Undo log 管"后悔",Redo log 管"不丢"。
-
Undo log:为"失败"而生
Undo log 记录的是:
"修改之前的数据是什么"
它解决的是一个核心问题:
❓如果事务执行到一半失败了,
我怎么把数据库恢复到"什么都没发生过"的状态?
工作方式非常直接:
- 每次修改数据前
- 先把"旧值"记录下来
- 一旦失败,就按日志反向恢复
-
Redo log:为"成功"而生
Redo log 记录的是:
"修改之后的数据是什么"
它解决的是另一个完全不同的问题:
❓事务已经告诉用户"成功了",
但机器突然断电了,怎么办?
数据库的答案是:
-
先写日志
-
再认为事务成功
-
即使数据页还在内存里,也能靠日志重放
-
-
把两者放在一起看(关键理解)
text事务开始 A 账户 1000 → 900 (记录 Undo:1000) B 账户 1000 → 1100 (记录 Undo:1000) 事务提交 Redo log 刷盘-
❌ 失败 → 用 Undo log 回滚
-
❌ 宕机 → 用 Redo log 重做
👉 正是这"两套日志",撑起了事务的原子性和持久性。
-
2.2 锁机制:事务隔离的真正来源
如果说日志解决的是"成与败",
那锁解决的就是另一个问题:
❓多个人同时操作同一份数据,会不会乱?
锁的本质是什么?
锁不是为了"慢别人",而是为了:
让并发世界,看起来像串行世界。
常见锁类型
| 锁类型 | 作用 | 特点 |
|---|---|---|
| 行锁 | 锁定一行数据 | 并发高,成本高 |
| 表锁 | 锁定整张表 | 简单,但并发低 |
如果没有锁,会发生什么?
两个人同时给同一个账户转账
-
都读到余额 100
-
都扣 80
-
最终余额变成 -60
👉 这不是"逻辑 bug",这是并发失控。
2.3 隔离级别:数据库如何在"正确"和"性能"之间取舍
数据库并不是非黑即白,而是给你提供了 4 种"妥协方案"。
| 隔离级别 | 能看到什么 | 会出现的问题 |
|---|---|---|
| 读未提交 | 别人没提交的数据 | 脏读 |
| 读已提交 | 已提交的数据 | 不可重复读 |
| 可重复读 | 多次读一致 | 幻读(部分实现避免) |
| 序列化 | 完全隔离 | 性能最差 |
👉 隔离级别的本质是:
你愿意为"更强一致性",付出多少性能代价。
2.4 缓冲区与提交机制:为什么事务不慢
如果每次事务都:
-
直接写磁盘
-
同步刷盘
那数据库早就不可用了。
所以真实流程是:
-
数据先写入内存缓冲
-
Redo log 顺序写磁盘
-
提交时对外"可见"
常见优化手段
-
批量刷盘(减少 IO)
-
异步刷盘(性能更高,风险更大)
👉 这也是为什么"持久性"在现实中是"可配置的"。
2.5 回滚机制:事务失败时,系统如何自救
事务失败时,数据库做的事情非常机械,但极其可靠:
-
查 Undo log
-
反向恢复数据
-
释放锁
⚠️ 一个常被忽略的事实:
即使系统崩溃,
下次启动时,数据库也会继续"未完成的回滚"。
3. 为什么单体事务无法跨库(这是分布式事务的起点)
到这里,你已经清楚:
单体事务之所以可靠,是因为:
- 日志
- 锁
- 提交顺序
全部在一个数据库内部完成
而一旦跨库,问题立刻出现。
跨库的三大致命问题
| 问题 | 表象 | 根本原因 |
|---|---|---|
| 原子性失效 | 一个库成功,一个失败 | 日志无法统一 |
| 网络不可靠 | 调用中断 | 节点随时可能失联 |
| 无全局协调 | 提交顺序不可控 | 没有统一事务管理者 |
一个非常形象的比喻
三个人搬苹果,每人负责一箱。
-
前两个人已经放好
-
第三个人半路摔倒
👉 结果不是"失败",而是"一半成功"。
而数据库事务存在的意义,恰恰是:
禁止这种世界状态出现。
4. 本部分你必须牢牢记住的几件事
核心认知锚点
-
事务不是魔法,是日志 + 锁 + 协调
-
ACID 的每一项,背后都有具体工程实现
-
单体事务的强一致性,来自"单一控制边界"
-
一旦跨库,所有保障机制立刻失效
高频易混理解澄清
-
Undo log ≠ Redo log
-
回滚 ≠ 删除数据
-
隔离级别 ≠ 正确与否,而是取舍
-
单体事务做不到的事,不是"设计不好",而是"边界决定"
自然引出下一问
❓既然单体事务做不到跨库,
那有没有一种方式,能把"事务"扩展到多个系统?
👉 这正是分布式事务诞生的原因。
第二部分:分布式事务的本质问题与 2PC 协议
一句话先立结论 :
分布式事务不是"数据库的问题",而是 多个自治系统之间如何达成一致的问题 。
而 2PC,是人类第一次用"协议"正面硬刚这个问题。
1. 分布式事务到底难在哪里
1.1 单体事务 vs 分布式事务:难度差异不是一个量级
在单体事务中:
-
日志、锁、缓冲区
-
都在同一个进程 / 同一个数据库
-
失败可控,恢复路径确定
但在分布式系统中:
❌ 没有"同一个地方"
❌ 没有"统一时钟"
❌ 没有"绝对可靠的网络"
1.2 一个典型失败场景
电商下单流程:
-
库存服务:扣减库存(成功)
-
订单服务:创建订单(未执行)
-
支付服务:扣款(失败)
最终状态:
| 服务 | 状态 |
|---|---|
| 库存 | ❌ 已扣 |
| 订单 | ❌ 不存在 |
| 支付 | ❌ 未扣 |
👉 这不是 Bug,而是分布式系统的"常态风险"
1.3 分布式事务失败的四个根因(非常重要)
| 根因 | 本质解释 |
|---|---|
| 原子性无法天然保证 | 每个服务只保证"本地原子性" |
| 网络不可靠 | 超时 ≠ 失败,失败 ≠ 不可达 |
| 节点随时宕机 | 服务、数据库、协调者都可能消失 |
| 没有天然的全局锁 | 锁、日志、MVCC 都是本地能力 |
💡 核心结论 :
分布式事务不是"怎么提交",而是 怎么在不可信环境中达成一致决策。
2. 2PC:第一个系统级解决方案
2.1 2PC 解决的不是"提交",而是"一致决策"
2PC(Two-Phase Commit)解决的是一个问题:
在多个参与者之间,如何保证:
- 要么大家一起提交
- 要么大家一起回滚
2.2 角色模型(必须记住)
| 角色 | 职责 |
|---|---|
| 协调者(Coordinator) | 全局事务决策者 |
| 参与者(Participant) | 本地事务执行者 |
| 事务日志 | 唯一可信的"记忆" |
⚠️ 注意:
协调者不是数据库的一部分,而是"协议角色"
3. 两阶段提交的完整流程(拆到原子级)
阶段一:Prepare(投票阶段)
目标只有一个:确认"有没有人会失败"
执行步骤:
-
协调者发送
Prepare请求 -
参与者:
-
开启本地事务
-
执行业务逻辑
-
写 Undo / Redo log
-
加锁,但不提交
-
-
返回:
-
YES(我能提交) -
NO(我必须回滚)
-
本质理解:
Prepare 阶段 = "锁资源 + 写日志 + 表态"
阶段二:Commit / Rollback(决策阶段)
分支一:全部 YES → Commit
-
协调者发送
Commit -
参与者:
-
提交本地事务
-
释放锁
-
清理 Undo log
-
分支二:任何 NO → Rollback
-
协调者发送
Rollback -
参与者:
-
根据 Undo log 回滚
-
释放锁
-
关键流程图
Coordinator
|
|--- Prepare ---> Participant A
|--- Prepare ---> Participant B
|<--- YES -------|
|<--- YES -------|
|
|--- Commit ----> Participant A
|--- Commit ----> Participant B
4. 2PC 的致命问题
4.1 为什么 2PC 是"阻塞型协议"
因为 Prepare 成功之后,参与者不能自己决定回滚
致命场景:
-
所有参与者返回 YES
-
协调者宕机
-
参与者状态:
| 项 | 状态 |
|---|---|
| 本地事务 | 未提交 |
| 锁 | ❌ 持有 |
| 是否可回滚 | ❌ 不敢 |
👉 只能"干等"
4.2 CAP 角度再看 2PC
2PC 的取舍是:
-
放弃可用性(A)
-
换取强一致性(C)
当网络分区或协调者宕机时:
❌ 宁可阻塞
❌ 也不允许出现不一致
5. 2PC 的优缺点
优点:
-
强一致性
-
理论完备
-
适合金融级事务
缺点:
| 问题 | 本质 |
|---|---|
| 阻塞 | 协调者单点 |
| 性能低 | 多次网络 + 日志 IO |
| 扩展性差 | 参与者越多,失败概率越高 |
6. 为什么 2PC 没有被广泛使用?
不是它不好,而是它"太理想主义"
从论文视角看,2PC 非常优雅:
-
协调者(Coordinator)全局视角
-
参与者(Participant)严格执行
-
只要协议走完,就能保证强一致
但问题是:
2PC 的前提条件,在现实系统中几乎从不成立。
6.1 2PC 的三个"理想前提"
-
协调者永远可靠
在 2PC 中,协调者是"上帝节点":
-
所有参与者都在等它的最终裁决
-
它一旦挂掉:
-
已锁资源的参与者 无事可做
-
协议直接进入"悬而未决"状态
-
现实世界:
-
JVM 会 OOM
-
Pod 会被 K8s 重启
-
机器会宕机、磁盘会坏
👉 单点协调者 = 单点灾难源
-
-
网络是稳定、可预测的
2PC 极度依赖网络可靠性:
-
Prepare 请求要全部成功返回
-
Commit / Rollback 必须"明确送达"
但现实是:
-
网络延迟不可控
-
TCP 断连 ≠ 对方是否收到
-
超时 ≠ 对方失败
于是就会出现:
-
协调者认为失败 → 回滚
-
某个参与者其实已经提交成功
👉 网络不可靠,让 2PC 的"确定性"失效
-
-
资源可以被长时间锁住
2PC 的本质:
先锁资源,再等所有人点头
在高并发系统中,这意味着:
-
数据库行锁 / 表锁长期占用
-
事务执行时间被"最慢节点"拖死
-
任何一个慢节点都会放大整体延迟
结果就是:
-
TPS 急剧下降
-
死锁概率上升
-
系统吞吐量崩溃
👉 在高并发系统中,2PC ≈ 主动制造性能瓶颈
-
6.2 2PC 的"致命工程缺陷"
把这些问题放到真实系统里,你会发现几个硬伤:
| 问题 | 工程后果 |
|---|---|
| 协调者单点 | 需要额外高可用设计 |
| 参与者阻塞 | 线程、连接、锁被长期占用 |
| 无法自动恢复 | 需要人工介入清理 |
| 延迟放大 | P99 / P999 极不稳定 |
这也是为什么:
-
数据库层面:只在极小范围内使用 2PC
-
微服务层面:几乎全面弃用
6.3 工业界的选择:放弃"强一致",拥抱"可用性"
现实系统的核心目标不是:
"任何时刻都绝对一致"
而是:
系统不挂、能恢复、能自愈
于是工业界达成了一个共识:
牺牲瞬时一致性,换取最终一致性
常见做法包括:
-
本地事务 + 消息通知
-
可靠消息最终一致性
-
补偿事务(TCC / Saga)
-
幂等 + 重试 + 对账
它们的共同点是:
-
不长时间锁资源
-
允许短暂不一致
-
失败可恢复
-
系统整体可用
6.4 一句话总结
2PC 适合论文、实验室和小范围强一致场景
但不适合高并发、分布式、易失败的真实世界
也正因为如此:
分布式事务的主战场,早已从 2PC 转向"最终一致性方案"
总结
到这里,你应该自然产生这些疑问:
-
❓ 能不能 不锁资源?
-
❓ 能不能 不用协调者阻塞等待?
-
❓ 能不能 通过业务补偿代替强回滚?
这正是后续方案出现的原因:
| 方案 | 核心思想 |
|---|---|
| 3PC | 降低阻塞 |
| TCC | 业务显式控制 |
| Saga | 反向补偿 |
| 本地消息表 | 数据最终一致 |
| 可靠消息 | 消息驱动事务 |
2PC 是分布式事务的"理论起点",
也是工业实践必须绕开的"性能天花板"。
第三部分:TCC ------ 把事务控制权从数据库交还给业务
如果说 2PC 是"让数据库帮我们保证分布式一致性",
那么 TCC (Try - Confirm/Cancel)的本质 是:
承认数据库做不到这一点,于是我们自己来做。
❗ 关键在于:TCC 是一套非常严谨、非常克制的业务事务模型
1. TCC 出现之前的核心问题
在 2PC 中,跨服务事务的问题不在于"一致性",而在于锁与阻塞:
-
锁在数据库层,不可控
-
锁时间长,轻微慢事务拖垮系统
-
协调者挂掉 → 所有资源阻塞
🔥 少量慢事务可能拖垮整个系统,这是互联网分布式环境的致命问题
问题归纳:
❗ 有没有一种方式,既能保证一致性,又不把数据库锁死?
TCC 的答案:
不要让数据库替你锁资源,而是让业务自己"显式管理资源状态"
知识点记忆:
-
2PC → 数据库锁资源 → 阻塞
-
TCC → 业务管理资源 → 弹性控制
2. TCC 核心思想:事务不是一瞬间完成的
单体事务模型:
text
开始事务 → 修改数据 → 提交 / 回滚
在分布式系统中,这个模型不现实
TCC 核心承认的事实:
❗ 跨服务事务,本质上是一个"过程",不是瞬间完成
三个业务阶段:
| 阶段 | 真正含义 | 核心目的 |
|---|---|---|
| Try | 资源预留 | 能不能做?先占住资源 |
| Confirm | 正式提交 | 确认执行,把事务做完 |
| Cancel | 回滚 | 不做了,把资源还回去 |
⚠️ 关键点:
Try 不是"尝试执行",而是"抢占资格"
3. 完整库存场景理解 TCC
3.1 错误直觉
很多人以为:
Try 阶段只是预扣,库存值不变 → 并发仍然能读到旧库存
结果:
❌ 必然超卖
并非 TCC 错,而是设计错了 TCC
3.2 正确 TCC 库存模型
至少需要三个字段:
text
stock
------
product_id
total_stock // 总库存
available_stock // 可用库存
frozen_stock // 冻结库存
并满足:
text
available_stock + frozen_stock = total_stock
4. Try 阶段到底改没改数据库?
结论:
❗ Try 一定会修改数据库,但只修改"业务可用资源"
不是 total_stock,而是 available_stock/frozen_stock
-
并发场景推演
初始状态:
texttotal = 10 available = 10 frozen = 0 -
请求 A:购买 6 件
sqlUPDATE stock SET available_stock = available_stock - 6, frozen_stock = frozen_stock + 6 WHERE product_id = 1 AND available_stock >= 6;结果:
textavailable = 4 frozen = 6 -
请求 B:并发购买 6 件
sqlavailable_stock >= 6 ❌ 不成立👉 Try 失败,自然挡住并发
知识点记忆:
- TCC 并发控制 不靠锁,靠原子条件更新 + 可用资源字段
5. 读旧数据问题解析
5.1 事务参与者
关心:
text
available_stock >= 下单数量
-
读的是 Try 之后的 available_stock
-
✅ 不存在旧数据问题
5.2 用户查询接口
-
❌ 不展示 total_stock / frozen_stock
-
✅ 展示 available_stock
-
用户看到的就是 Try 之后的结果
🔥 TCC 不存在"读旧数据"问题,只要设计正确
6. TCC 的"锁"在哪里?
| 对比项 | 数据库事务锁 | TCC 冻结 |
|---|---|---|
| 锁的位置 | 数据库内核 | 业务字段 |
| 锁形式 | 行锁 / 表锁 | 状态位 / 数量字段 |
| 可见性 | 不可见 | 完全可见 |
| 生命周期 | 不可控 | 业务可控 |
| 是否可补偿 | 否 | 是 |
本质理解:
🔥 TCC = 把隐式锁 → 显式状态,交给业务可控
7. Confirm 与 Cancel:事务真正结局
Confirm 阶段
text
frozen_stock -= n
total_stock -= n
- 真正数据提交
Cancel 阶段
text
frozen_stock -= n
available_stock += n
- 业务层回滚
知识点:
- Confirm / Cancel 才是 TCC 的最终结果
- Try 只是资源占用
8. TCC 的三个隐藏难点
-
幂等性
- Confirm / Cancel 可能被重复调用
- 必须能重复执行而不出错 → 状态判断
-
空回滚
- Cancel 先于 Try 到达
- 必须允许什么都不做直接成功
-
悬挂问题
- Cancel 执行完 → Try 又来了
- Try 必须检查是否已 Cancel
9. Try 成功但 Confirm 不来怎么办?
现实系统一定会发生
解决方案:
-
冻结记录带时间戳
-
定时任务扫描
-
超时自动 Cancel
text
冻结超过 30 分钟 → 自动释放
🔥 这是 TCC 的设计本意,不是异常
10. TCC 的适用场景与限制
适合
-
库存冻结
-
资金冻结
-
支付扣款
-
强一致性业务
不适合
-
长流程(分钟 / 小时级)
-
无法预留资源的场景
-
简单 CRUD
11. 核心记忆总结
🔥 TCC 并不是数据库事务替代品,而是业务承担事务复杂性的一种方式
特点:
-
并发控制 → 原子更新 + 可用资源字段
-
避免阻塞 → 锁交给业务
-
异常处理 → Confirm / Cancel + 超时策略
-
幂等 & 状态机 → 保障可靠性
第四部分:Saga ------ 接受不一致,用时间换系统存活
如果说:
-
2PC:一致性优先,系统其次
-
TCC:一致性和性能的平衡
那么 Saga 的哲学是:
❗ 系统活着,比数据立刻一致更重要
1. Saga 出现的根本原因:长事务 TCC 承受不了
在之前的章节,你已经理解了:
-
TCC 通过冻结资源保证一致性
-
冻结本质是一种 业务级锁
问题来了:
❓ 如果这个"事务"持续 几分钟、几小时、甚至几天,系统该怎么办?
现实问题示例
| 场景 | 步长 | 时间 |
|---|---|---|
| 创建订单 | 写订单表 | 秒 |
| 扣库存 | 减库存、冻结库存 | 秒 |
| 支付 | 扣款、冻结资金 | 秒 |
| 拣货 | 仓库操作 | 几小时 |
| 发运 | 物流配送 | 几天 |
| 用户签收 | 确认收货 | 几天 |
TCC 的限制:
长时间冻结资源意味着:
-
库存锁定几天 → 其他订单无法操作
-
钱冻结几天 → 用户无法使用余额
-
系统资源压力大 → 易堵塞
🔑 结论:TCC 不适合长事务场景
2. Saga 的核心思想:事务不是"一次性提交"
Saga 的第一性原理是:
❗ 分布式事务 = 一连串已经发生的事实
你无法让"已经发生的事情"消失,只能:
-
接受它
-
或 用另一个行为抵消它
原理解析
-
2PC / TCC 的事务依赖锁或协调者保证"一致性瞬间达成"
-
长事务会导致锁超时、冻结资源过久、系统压力过大
-
Saga 放弃瞬时一致,用 补偿事务(Compensation) 替代回滚
关键理解 :Saga 不是回滚旧事务,而是 执行新事务抵消影响。
3. Saga 的基本模型
结构示意
text
T1 → T2 → T3 → T4
↑ ↑ ↑
C1 C2 C3
-
T:正向事务
-
C:补偿事务
电商场景示例
| 正向事务 | 补偿事务 |
|---|---|
| T1:创建订单 | C1:取消订单 |
| T2:扣库存 | C2:恢复库存 |
| T3:扣款 | C3:退款 |
| T4:发货 | C4:拦截或退货 |
底层原理:
-
每个本地事务依然是单体事务,保持本地 ACID
-
Saga 将全局一致性拆解为 一系列本地事务 + 补偿
-
不依赖锁或协调者长时间阻塞资源
记忆点 :Saga 的核心 = 顺序执行 + 补偿
4. Saga 与 TCC 的本质对比
| 对比点 | TCC | Saga |
|---|---|---|
| 一致性 | 强一致 | 最终一致 |
| 是否冻结资源 | 是 | 否 |
| 事务时长 | 秒级 | 分钟 / 小时 / 天 |
| 回滚方式 | Cancel(业务回滚) | 补偿事务(新业务逻辑) |
| 系统压力 | 中 | 低 |
| 并发控制 | 数据库锁 / 条件更新 | 业务状态校验 |
🔑 核心理解:Saga 用 允许短暂不一致 换 系统可用性
可以在文档里加一些实际业务场景的例子,这样读者更容易理解两种 Saga 的差异。我帮你补充如下:
5. Saga 的两种实现方式
编排式 Saga(Orchestration)
-
有中心协调器(Saga Coordinator)
-
统一控制事务执行顺序
-
失败时发起补偿事务
优点:
-
流程清晰
-
易监控
-
可追踪全局事务状态
缺点:
-
中心节点压力大
-
中心故障需要额外容灾
-
难以水平扩展
例子 :
电商下单流程:
-
用户下单 → Saga Coordinator 接收请求
-
扣减库存 → 扣款 → 创建物流订单(顺序控制)
-
如果扣款失败 → Saga Coordinator 发起补偿事务(回滚库存)
这里协调器负责整个流程的顺序控制和补偿逻辑,整个事务链条清晰可追踪。
事件驱动 Saga(Choreography)
-
服务间通过事件通信(事件总线)
-
无中心协调者
优点:
-
松耦合
-
易扩展
-
服务自治
缺点:
-
调试困难
-
逻辑分散,事件追踪复杂
-
容易出现事务漏补偿
例子 :
同样是电商下单流程:
-
下单服务发出
OrderCreated事件 -
库存服务监听事件 → 扣减库存 → 发布
InventoryReserved或InventoryFailed -
支付服务监听
InventoryReserved→ 扣款 → 发布PaymentCompleted或PaymentFailed -
物流服务监听
PaymentCompleted→ 创建物流订单
每个服务独立处理事件并发布结果,没有中心协调器。流程松耦合,容易扩展,但如果某个事件丢失或者处理失败,可能导致事务不一致,需要额外设计补偿机制。
6. Saga 的关键工程难点
| 难点 | 说明 | 解决方法 |
|---|---|---|
| 补偿不一定成功 | 已发生事实不可撤销 | 人工介入或业务逻辑补偿 |
| 幂等 & 重试 | 事件可能重复触发 | 补偿事务设计幂等、记录状态 |
| 中间态存在 | 系统短期内不一致 | 状态机 + 状态校验规则 |
| 并发问题 | 多事务同时执行可能冲突 | 业务约束:状态检查、顺序控制 |
原理解析:
-
补偿事务必须能安全重复执行 → 幂等性
-
系统允许短期不一致 → 最终一致性
-
并发控制交给业务逻辑 → 不依赖数据库锁
7. Saga 的适用场景
| 场景类型 | 特点 |
|---|---|
| 长流程事务 | 跨系统、分钟至天级事务 |
| 无法冻结资源 | 库存、资金、长时间外部操作 |
| 最终一致性可接受 | 电商订单、支付 + 物流、积分兑换 |
| 高可用要求 | 系统不能因为事务阻塞而瘫痪 |
不适用:
-
短事务要求强一致性 → 2PC / TCC 更优
-
无法设计补偿逻辑的场景 → Saga 不适用
8. 工程实践技巧
-
幂等设计:确保补偿或重试不会重复扣款或重复库存调整
-
状态机设计:用订单/业务状态约束操作,防止非法跳转
-
日志/事件追踪:记录每步事务状态,便于问题排查
-
补偿策略:自动 vs 人工,分清可自动补偿和需要人工介入的步骤
-
监控告警:事务长时间未完成 → 警报或触发补偿
9. 总结与记忆点
-
Saga 核心思想:放弃瞬时一致性 → 接受不一致 → 用补偿事务保证最终一致
-
TCC vs Saga:TCC 锁资源、强一致;Saga 不锁资源、最终一致
-
事务粒度:Saga 拆分成多个本地事务 → 每个事务独立保持 ACID
-
工程难点:幂等、补偿失败、中间态、并发控制
-
适用场景:长流程事务、无法冻结资源、系统高可用、可接受最终一致性
🔥 核心哲学 :系统活着,比数据立刻一致更重要。
选择分布式事务方案 = 在一致性、性能、复杂性之间做取舍
第五部分:可靠消息 / 事务消息 ------ 把一致性交给时间
如果说:
-
2PC 是"强一致但脆弱"
-
TCC 是"强一致但复杂"
-
Saga 是"接受不一致但仍然痛苦"
那么 可靠消息的哲学是:
❗ 只要最终一致即可,过程不一致是常态
这不是妥协,而是对现实世界的尊重。
1. 先抛结论:为什么消息几乎是最终答案?
在真实的分布式系统中,最致命的三件事是:
-
同步调用阻塞
-
中央协调节点挂掉
-
跨服务锁资源
这些问题并不是"实现不好",而是系统规模一旦变大就必然出现。
而消息天然具备的特性,正好对症下药:
-
异步:不等结果,先往前走
-
解耦:服务之间不再直接"绑死"
-
可重试:失败不是终点,只是延后
-
可削峰:流量不再同时压垮系统
👉 所以你会发现一个规律:
几乎所有大规模系统,最终都会演进到「本地事务 + 消息」
这不是潮流,是幸存者路径。
本节记忆总结
-
同步 ≠ 安全,只是看起来简单
-
消息不是为了"方便",而是为了活下来
-
系统越大,越依赖异步
2. 一个最真实的业务问题(几乎人人都踩过)
场景:下单 + 扣库存
text
服务 A:订单服务(写订单表)
服务 B:库存服务(扣库存)
这是所有电商、内容、课程、权益系统的基础模型。
最天真的实现(必出问题)
text
1. 写订单
2. 调库存接口扣库存
你可能觉得"逻辑没问题",但现实会告诉你:
-
库存扣了,订单写失败?
-
订单写了,库存扣失败?
-
网络抖了一下,结果到底成功没?
👉 问题不是"怎么兜底"
👉 问题是:你根本不知道现在是什么状态
本节记忆总结
-
分布式系统中,"不知道"本身就是一种失败
-
同步调用只是在掩盖不确定性
3. 为什么"数据库事务 + 发消息"一定会失败?
很多人第一反应是:
text
BEGIN;
INSERT INTO order;
SEND MQ MESSAGE;
COMMIT;
看起来很合理:
"要么都成功,要么都失败"。
但现实是:
❌ 数据库事务 和 MQ 没有共同的事务上下文
它们属于两个完全不同的世界。
一定会发生的失败场景
-
DB 提交成功,消息没发出去
- 下游永远不知道这笔订单
-
消息发出去了,DB 回滚了
- 下游收到一条"幽灵消息"
👉 这不是 Bug,而是系统边界问题。
本节记忆总结
-
跨系统不存在"天然原子性"
-
只要不是同一个资源管理器,就不存在真正的事务
4. 可靠消息的核心模式:本地事务 + 消息表(关键转折)
这是整个分布式事务体系里最重要的一步。
1️⃣ 核心思想(一句话)
把"发消息"也当成一条业务数据
既然消息不可靠,那就先别发,先把"要发什么"记录下来。
2️⃣ 表结构示意(极简但够用)
text
order
----
id
status
message_outbox
----
msg_id
topic
payload
status // NEW / SENT
这张 message_outbox 表,就是可靠性的根源。
3️⃣ 正确的执行流程(必须理解透)
Step 1:本地事务(原子保证)
text
BEGIN;
INSERT order;
INSERT message_outbox(status = NEW);
COMMIT;
✔️ 订单和消息记录要么一起成功
✔️ 要么一起失败
此时还没有真正发消息。
Step 2:异步投递(解耦关键)
- 后台线程 / 定时任务扫描
message_outbox - 把
NEW状态的消息发送到 MQ - 发送成功 → 标记为
SENT
业务线程已经结束,不再被阻塞。
本节记忆总结
- 可靠性的本质:先落盘,再异步
- 消息表 = 自己的"消息保险箱"
5. 这套模型为什么"天然可靠"?
我们用最残酷的方式看失败。
场景 1:事务提交失败
- 订单没写
- 消息没记录
👉 等价于:什么都没发生
场景 2:事务成功,但服务宕机
- 订单在
- 消息在
- 还没来得及发
服务重启后:
- 扫描消息表
- 继续发送
👉 消息不会丢
场景 3:消息发出,但 ACK 丢失
- MQ 实际已收到
- 本地仍是
NEW - 会再次发送
👉 必然重复
所以结论只有一个:
❗ 消费端必须幂等
本节记忆总结
- 不丢消息 ≠ 不重复消息
- 重复是"设计结果",不是异常
6. 那"消息重复"怎么办?(必须吃透)
核心认知
❗ 消息系统默认就是「至少一次」
任何"只消费一次"的幻想,都会在规模化后破灭。
常见幂等实现方式
-
唯一业务 ID
- 订单号、流水号
-
去重表
- 消费过就记下来
-
状态机校验
- 已完成状态直接忽略
👉 幂等不是 MQ 的责任,是业务设计的一部分。
本节记忆总结
- 没有幂等,就没有可靠消息
- 幂等 = 把"重复"当正常情况处理
可以,把你的 RocketMQ 事务消息示意改为 RabbitMQ 版本,思路是一样的:都是"消息表 + 本地事务 + 回查/补偿",只不过 RabbitMQ 自身没有内置事务消息机制,需要自己实现半消息/确认 + 状态回查。改写如下示例:
7. 事务消息(以 RabbitMQ 思想为例)
事务消息并不是新理论,而是:
"消息表模式"的中间件化实现
事务消息流程(高度抽象)
-
保存半消息到数据库(消息未投递到 RabbitMQ)
-
执行本地事务(如更新业务表)
-
事务成功 → 投递消息到 RabbitMQ
事务失败 → 删除半消息 / 不投递
-
异步回查事务状态(可用定时任务扫描消息表,重试投递或补偿)
text
[业务库操作] ---> 本地事务成功 ---> 投递消息 ---> MQ 消费者消费
| ^
|--- 本地事务失败 ----------------|
👉 消息是否可见,取决于本地事务是否成功
本质没有变
-
只是把:
-
消息表
-
状态回查
-
重试逻辑
-
交给了应用层 + RabbitMQ 托管。
本节记忆总结
-
事务消息不是"魔法"
-
只是把土方案标准化
-
与 RocketMQ 不同,RabbitMQ 没有内置事务消息机制,需要自己实现"半消息 + 回查"
8. 可靠消息 vs TCC vs Saga(最终对照)
| 维度 | 可靠消息 | TCC | Saga |
|---|---|---|---|
| 本质目标 | 消息可靠投递 / 最终送达 | 强一致性,补偿可选 | 最终一致性,补偿必须 |
| 事务粒度 | 单条消息 | 业务操作 | 业务操作序列 |
| 是否全局锁 | ❌ 不锁业务资源 | ✅ Try 阶段可锁资源 | ❌ 不锁,依赖补偿 |
| 是否有全局共识阶段 | ❌ 没有 | ✅ 有(Try / Confirm / Cancel) | ❌ 没有,依靠补偿 |
| 失败处理 | 重试 / 死信 / 补偿可选 | 回滚(Cancel) | 执行补偿事务 |
| 一致性保证 | 最终一致性(消息投递) | 强一致性(Try + Confirm / Cancel) | 最终一致性(补偿事务收敛) |
| 典型场景 | MQ 事务消息、可靠消息表模式 | 金融交易、库存扣减 | 订单履约、长流程审批、跨系统任务 |
| 实现复杂度 | 中(依赖 MQ + 状态回查) | 高(每个业务操作需设计 Try/Confirm/Cancel) | 中高(需要流程编排 + 补偿事务) |
| 时效性 | 高(消息投递异步) | 中低(可能占用锁,阻塞 Try 阶段) | 中(依赖流程执行和补偿) |
👉 消息是默认选项,TCC 是少数特权
解释核心差异
- 可靠消息
-
其实是 消息一致性工具,保证消息不会丢、不会重复。
-
它 不直接保证业务操作原子性,只是把"消息投递和本地事务尽量结合起来"。
-
常用方式:
-
消息表模式 + 定时扫描(RabbitMQ / RocketMQ 可实现)
-
MQ 事务消息(RocketMQ 内置半消息 + 回查)
-
-
✅ 适合 异步解耦 + 最终一致性
-
❌ 不是严格意义上的分布式事务
- TCC
-
Try / Confirm / Cancel 三阶段
-
核心是 在提交前形成全局共识
-
❌ 不能跨长时间(资源被锁定)
-
✅ 强一致性,适合短事务场景
- Saga
-
核心是 业务事务序列 + 补偿事务
-
❌ 没有全局锁,也没有共识阶段
-
✅ 能跨天/跨系统/人工参与
-
✅ 适合长事务 / 最终一致性场景
一句话总结
-
可靠消息 = "保证消息送达",不保证业务原子性
-
TCC = "严格一致性",资源锁定 + Try/Confirm/Cancel
-
Saga = "长事务最终一致性",依赖补偿事务收敛
9. 可靠消息的隐性成本
1️⃣ 延迟
- 一致性是"迟到的正确"
- 不适合强实时场景
2️⃣ 状态复杂度
- 订单状态
- 消息状态
- 补偿状态
系统复杂度真实存在。
3️⃣ 运维成本
- 消息堆积
- 重试风暴
- 死信处理
消息不是银弹,只是可控的麻烦。
本节记忆总结
- 没有"零成本一致性"
- 只是把风险换了种形态
第六部分:如何在真实系统中选择分布式事务方案
真正成熟的工程能力,不是知道所有方案,
而是知道:在什么地方,必须妥协。
一条连续的发展线:上一阶段解决了什么 → 暴露了什么问题 → 下一阶段为什么出现
| 阶段 | 出现原因(上一阶段的问题) | 底层原理 / 机制 | 优点 | 缺点 | 优化方案 | 优化后仍存在问题 | 适用场景 |
|---|---|---|---|---|---|---|---|
| 单体事务 | 业务集中在单体系统、单数据库 | ACID;undo/redo log;锁/MVCC;WAL | 实现简单;强一致性 | 只能单库;无法跨服务 | 无需优化 | 一旦拆库拆服务,事务失效 | 单体应用;金融单库核心账务 |
| 2PC | 单体事务无法跨库/跨服务 | Coordinator + Participant;Prepare / Commit;持久化日志;资源锁定 | 强一致性;支持跨库 | 阻塞;性能低;强依赖网络与协调者 | 引入 预提交 | 网络分区仍可能不一致;通信成本高 | 核心金融系统;低并发高一致性 |
| 3PC | 2PC 协调者宕机导致阻塞 | CanCommit → PreCommit → DoCommit;超时自决 | 减少阻塞;参与者可超时处理 | 通信轮次多;复杂度高 | 放弃绝对强一致,引入业务级事务 | 网络分区仍不可靠;工程复杂 | 银行核心交易;高可靠系统 |
| TCC | 3PC 在微服务下性能和复杂度不可接受,但仍需强一致性 | Try 冻结资源;Confirm 提交;Cancel 释放 | 接近强一致性;业务可控 | 开发成本高;资源长期冻结 | 自动化 TCC 框架;精细化冻结 | 高并发下资源压力大 | 资金;高价值库存;账户系统 |
| Saga(编排式) | TCC 过重,微服务下阻塞成本高 | 本地事务 + 补偿事务;中心协调器;状态机 | 非阻塞;高性能;流程清晰 | 补偿复杂;短时不一致 | 状态机固化流程;幂等与重试 | 补偿并非严格逆操作 | 电商订单;库存;支付流程 |
| Saga(事件式) | 编排式中心协调器成为瓶颈 | 事件驱动;消息队列;服务自治 | 去中心化;松耦合;易扩展 | 消息乱序/丢失;排错困难 | 事件追踪;可观测性建设 | 调试复杂;一致性可见性差 | 大型微服务体系 |
| 消息驱动最终一致性 | Saga 在海量系统下仍有复杂度 | 本地事务 + 可靠消息;幂等消费;重试 | 高吞吐;高可用;高扩展 | 只能最终一致;补偿链路长 | 业务对账;异步修复;监控告警 | 无法实时强一致 | 海量订单;日志;社交系统 |
1. 先给你一个残酷但真实的结论
❗ 99% 的业务,其实根本不需要"分布式事务"
你之所以"感觉需要",往往是因为:
- 服务拆分过早
- 边界没想清楚
- 想让系统"看起来很干净"
2. 为什么"单体事务"能解决的问题,千万别拆?
单体事务的真正优势(很多人低估了)
- ACID 是数据库几十年工程沉淀
- 日志、锁、恢复、隔离级别全都帮你做好了
- 出问题可以靠 DBA、备份、回滚
👉 能在一个库里完成的事,千万别拆出去
3. 什么时候你"被迫"进入分布式事务?
只有当以下情况同时成立:
- 数据量 / 组织结构 / 技术边界 必须拆库拆服务
- 操作横跨多个独立服务
- 不能接受随意失败或不一致
👉 你才真正进入了分布式事务领域
4. 一套真实可用的"选择决策树"**
请你把下面这套逻辑记成条件反射。
第一步:能不能"异步最终一致"?
如果可以:
👉 优先:可靠消息 / 事务消息
- 电商
- 内容系统
- 用户行为
- 统计数据
为什么?
- 简单
- 稳定
- 扩展性最好
第二步:是否涉及"钱 / 库存 / 额度"?**
如果是:
问下一个问题:
能不能接受"冻结"?
第三步:能不能冻结资源?
如果能冻结(秒级):
👉 TCC
- 资金冻结
- 库存锁定
- 配额预占
如果不能冻结或时间太长:
👉 Saga
5. 一个完整的现实系统是怎么混用的?**
你以为系统只能选一种?
现实是:同时存在 3~4 种事务模型。
电商真实拆解示意:
| 模块 | 方案 |
|---|---|
| 下单 → 写订单 | 本地事务 |
| 下单 → 扣库存 | TCC |
| 下单 → 发通知 | 消息 |
| 履约 → 发货 | Saga |
| 统计 → 报表 | 最终一致 |
👉 没有银弹,只有组合拳
6. 最常见、最致命的 5 个误区**
❌ 误区 1:追求"全局强一致"
现实:
你会得到一个慢、脆、贵的系统
❌ 误区 2:到处用 TCC
现实:
- 代码爆炸
- 测试地狱
- 运维崩溃
❌ 误区 3:把 MQ 当数据库
现实:
- 状态丢失
- 排查困难
❌ 误区 4:忽略幂等和重试
现实:
第一次不出问题,
第 100 次必出事故
❌ 误区 5:设计时不考虑失败
现实:
分布式系统中,失败才是常态
7. 你必须形成的三种"工程直觉"
所有远程调用都可能失败
所以:
- 必须可重试
- 必须幂等
所有一致性都是"设计出来的"
不是数据库帮你保证
是你用 状态 + 规则 + 补偿 保证
数据不一致 ≠ 系统失败
真正的失败是:
系统无法自愈
8. 如果让你"从零设计一个系统",该怎么做?
一个成熟的步骤是:
-
先单体
-
用数据库事务兜底
-
只在瓶颈处拆服务
-
拆了之后:
-
默认消息
-
关键点 TCC
-
长流程 Saga
-
9. 你现在应该真正"吃透"的分布式事务观
请你记住这段话:
🔥 分布式事务不是技术问题,是"选择问题"
🔥 一致性不是免费的,它用性能、复杂度、可用性换
🔥 工程的最高境界,是知道哪里"不值得完美"
把简单问题留给数据库,
把复杂问题交给业务,
把不可控问题交给时间。