分布式系统——为什么单体时代过去了 / Distributed Systems Beyond the Monolith
📅 创建时间:2026-05-08 🏷️ 标签:#分布式 #CAP #BASE #Raft #Paxos #分布式ID #分布式事务 📚 前置知识:[[00-backend-overview]] [[01-redis-deep]] [[02-mq-kafka]] 📚 相关知识:[[/03-web/06-databases-and-data-access/06-kv-embedded]](Raft 协议) [[/03-web/06-databases-and-data-access/07-distributed-sql]](NewSQL)
场景:你的单体系统撑不住了
┌─────────────────────────────────────────────────────────────┐
│ │
│ 双十一前一个月,你的单体电商系统开始出问题: │
│ │
│ 问题 1:数据库 CPU 100%,单表 5000 万行 │
│ → 加索引没用,数据量太大了 │
│ │
│ 问题 2:订单服务和用户服务"互相影响" │
│ → 订单服务挂了 → 用户服务也挂了(共享进程) │
│ │
│ 问题 3:发布一次需要停机 2 小时 │
│ → 改一行代码,全系统回归测试 │
│ │
│ CTO 说:拆! │
│ │
└─────────────────────────────────────────────────────────────┘分布式系统解决了单体的问题,但也带来了新的问题。这一章,我们理解分布式系统最核心的挑战。
第1节:CAP 定理——分布式系统的不可能三角
问题抽象
三个人分两间教室考试(两个节点)。突然停电了(网络分区)。
┌─────────────────────────────────────────────────────────────┐
│ │
│ 选择 A:发卷子(一致性) │
│ 停电了,发不了卷子 → 学生等(不可用) │
│ │
│ 选择 B:发备用卷子(可用性) │
│ 备用卷和正式卷不一样 → 数据不一致 │
│ │
│ 鱼和熊掌不可兼得。 │
│ │
└─────────────────────────────────────────────────────────────┘推演:CAP 到底是什么
┌─────────────────────────────────────────────────────────────┐
│ CAP 三角:三个只能同时满足两个 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Consistency (C) │
│ ▲ │
│ /│\ │
│ / │ \ │
│ / │ \ │
│ / │ \ │
│ / │ \ │
│ ▼─────┼─────▼ │
│ Availability ◄───┴───► Partition Tolerance │
│ (可用性) (分区容错) │
│ │
│ C + A(放弃 P):传统单机数据库 │
│ → 不存在分区(单台机器),所以 C 和 A 可以同时满足 │
│ → MySQL、PostgreSQL、Redis(单机模式) │
│ │
│ C + P(放弃 A):强一致系统 │
│ → 分区发生时,系统停止服务(返回错误) │
│ → ZooKeeper、etcd、CockroachDB(强一致模式) │
│ │
│ A + P(放弃 C):最终一致系统 │
│ → 分区发生时,系统继续服务,但数据可能不一致 │
│ → Cassandra、DynamoDB、MongoDB(副本集默认模式) │
│ │
└─────────────────────────────────────────────────────────────┘常见误解澄清
❌ 误解:CAP 意味着"三选二"
✅ 真相:正常运行时(无分区),系统可以同时满足 C 和 A。
只有发生分区时,才被迫选择。
❌ 误解:分布式系统一定要选 AP 或 CP
✅ 真相:P(分区容错)是分布式系统的固有属性,无法放弃。
所以实际是:在 C 和 A 之间选择。
❌ 误解:MySQL 比 Redis 更"一致"
✅ 真相:单机的 MySQL 因为没有分区,可以同时满足 C 和 A。
但它不是分布式系统。第2节:BASE 理论——现实的妥协
问题抽象
MySQL 的强一致性很好,但在分布式系统下代价太高(2PC 要锁资源)。
现实中:我们愿意接受"最终一致",只要系统最终是对的就行。
BASE 的含义
┌─────────────────────────────────────────────────────────────┐
│ BASE 理论三要素 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Basically Available(基本可用): │
│ 系统出现故障时,允许降级服务,但核心功能可用。 │
│ 例:Redis 从节点挂了,主节点继续服务 │
│ │
│ Soft state(软状态): │
│ 系统状态在不同节点间同步时,允许存在中间状态。 │
│ 例:主从复制的延迟期间,主从数据可能短暂不一致 │
│ │
│ Eventually consistent(最终一致): │
│ 系统在故障恢复后,经过一段时间,数据会达到一致。 │
│ 例:MySQL 主从复制完成后,从库数据和主库一致 │
│ │
└─────────────────────────────────────────────────────────────┘
对比:
强一致(MySQL 事务):写入后,所有人立即看到同样的数据
最终一致(BASE):写入后,经过一段时间,所有人看到同样的数据一致性模型对比
┌─────────────────────────────────────────────────────────────┐
│ 一致性模型从强到弱的排序 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 强一致 → 顺序一致 → 因果一致 → 最终一致 → 弱一致 │
│ ↑ │
│ MySQL Redis 分布式 KV Cassandra │
│ 事务 主从同步 主从最终一致 │
│ │
│ 强一致:所有节点同一时刻看到同样的数据 │
│ 顺序一致:所有节点按相同顺序看到所有操作 │
│ 因果一致:因果相关的操作按顺序,不同操作可并发 │
│ 最终一致:经过足够时间,所有节点数据一致 │
│ │
│ 越强的一致性 = 越多的协调 = 越低的性能和可用性 │
│ │
└─────────────────────────────────────────────────────────────┘第3节:Raft 共识算法——如何让多个节点达成一致
场景:三个人决定谁当队长(选主问题)
┌─────────────────────────────────────────────────────────────┐
│ │
│ 三个人分别在北京、上海、广州(三台服务器) │
│ │
│ 正常情况:多数派决定(2/3 同意即通过) │
│ │
│ 网络分区情况: │
│ 北京 ←→ 上海 | 广州(广州和北京、上海断开了) │
│ │
│ 北京 + 上海:多数派(可以选主、可以写数据) │
│ 广州:少数派(不能选主、不能写数据) │
│ │
└─────────────────────────────────────────────────────────────┘Raft 选主流程
┌─────────────────────────────────────────────────────────────┐
│ Raft 选主流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 初始状态:三台都是 Follower │
│ │
│ Step 1: 选举超时(每个节点随机 150-300ms) │
│ 节点 A 超时 → 成为 Candidate → 开始选举 │
│ │
│ Step 2: 投票请求 │
│ A ──投票请求──▶ B (A 有更新的日志) → B 同意 ✅ │
│ A ──投票请求──▶ C (A 有更新的日志) → C 同意 ✅ │
│ │
│ Step 3: 获得多数票 → 成为 Leader │
│ A 获得 2 票(自己 + B)→ 成为 Leader │
│ │
│ Step 4: 心跳保活 │
│ A 定期发心跳给 B、C → "我还是 Leader,别重新选举" │
│ │
│ 如果 A 挂了: │
│ B、C 的选举超时触发 → 其中一个成为 Candidate → 重新投票 │
│ │
└─────────────────────────────────────────────────────────────┘Raft vs Paxos
┌─────────────────────────────────────────────────────────────┐
│ Raft vs Paxos 对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Paxos(Leslie Lamport,1998): │
│ • 理论证明完整,实际系统难以直接实现 │
│ • Multi-Paxos 优化复杂 │
│ • Zookeeper 使用的是 Paxos 的变种(ZAB) │
│ │
│ Raft(Diego Ongaro,2014): │
│ • 从 Paxos 简化而来,核心思想相同 │
│ • 更好理解,更容易实现 │
│ • etcd、CockroachDB、TiKV 使用 Raft │
│ │
│ 面试中问 Paxos:理解 Basic Paxos 的两个阶段 │
│ • Prepare:提案编号 N,询问"我可以用 N 吗?" │
│ • Accept:多数派同意后,确认"就用 N 了" │
│ │
└─────────────────────────────────────────────────────────────┘第4节:分布式 ID 生成——如何生成不重复的 ID
场景:订单 ID 不能重复
单体系统:直接用数据库自增 ID
分布式系统:多个节点同时生成 ID → 可能冲突
雪花算法(Snowflake):
时间戳(41 位) 机器 ID(10 位) 序列号(12 位)
|________________|_____|______|
2026-05-08 节点 5 0-4095
每毫秒每个节点可以生成 4096 个不重复 ID
理论上:69 年内不重复雪花算法实现
python
import time
class Snowflake:
def __init__(self, worker_id):
self.worker_id = worker_id
self.sequence = 0
self.last_timestamp = -1
self.epoch = 1704067200000 # 2024-01-01
def _current_millis(self):
return int(time.time() * 1000)
def next_id(self):
ts = self._current_millis()
if ts == self.last_timestamp:
self.sequence = (self.sequence + 1) & 4095 # 序列号轮回
if self.sequence == 0:
# 同一毫秒内序列号用完了,等下一毫秒
while ts <= self.last_timestamp:
ts = self._current_millis()
else:
self.sequence = 0
self.last_timestamp = ts
# 组装 ID:时间差 << 22 | worker_id << 12 | sequence
return ((ts - self.epoch) << 22) | (self.worker_id << 12) | self.sequence分布式 ID 方案对比
┌─────────────────────────────────────────────────────────────┐
│ 分布式 ID 方案对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ │ 方案 │ 优点 │ 缺点 │
│ ├───────────────┼────────────────┼───────────────────────┤
│ │ UUID │ 简单,不依赖 │ 无序,36 字符太长 │
│ │ │ 任何服务 │ │
│ ├───────────────┼────────────────┼───────────────────────┤
│ │ 雪花算法 │ 有序,趋势递增 │ 依赖时钟(时钟回拨)│
│ │ (Snowflake) │ 性能高 │ 依赖机器 ID 配置 │
│ ├───────────────┼────────────────┼───────────────────────┤
│ │ 数据库自增 │ 简单,绝对有序 │ 性能有限,依赖单点 │
│ ├───────────────┼────────────────┼───────────────────────┤
│ │ 百度 UidGenerator│ 趋势递增, │ 实现复杂 │
│ │ │ 时钟友好 │ │
│ └───────────────┴────────────────┴───────────────────────┘
│ │
└─────────────────────────────────────────────────────────────┘第5节:分布式事务——跨系统的原子性
场景:下单 + 扣库存 + 扣余额,必须同时成功或失败
单体系统:一个事务搞定
分布式系统:
服务 A(订单服务)──INSERT orders──▶ MySQL
服务 B(库存服务)──DECR stock──▶ Redis
服务 C(支付服务)──DECR balance──▶ 账户数据库
如果 B 成功了,C 失败了 → 库存扣了但钱没扣 → 数据不一致!方案一:2PC(两阶段提交)
┌─────────────────────────────────────────────────────────────┐
│ 2PC 两阶段提交 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 协调者(事务管理器): │
│ │
│ Phase 1: Prepare │
│ 协调者 ──▶ 服务 A: "你们准备好了吗?" │
│ 协调者 ──▶ 服务 B: "你们准备好了吗?" │
│ 协调者 ──▶ 服务 C: "你们准备好了吗?" │
│ A: 准备好了(锁定资源) │
│ B: 准备好了(锁定资源) │
│ C: 还没准备好 │
│ │
│ Phase 2: Commit / Rollback │
│ 协调者 ──▶ C: "回滚" │
│ 协调者 ──▶ A: "提交" │
│ 协调者 ──▶ B: "提交" │
│ │
│ 问题: │
│ ❌ 单点故障:协调者挂了,所有服务资源锁定 │
│ ❌ 同步阻塞:Prepare 阶段所有服务阻塞 │
│ ❌ 数据不一致:部分服务提交成功,部分回滚 │
│ │
└─────────────────────────────────────────────────────────────┘方案二:TCC(Try-Confirm-Cancel)
┌─────────────────────────────────────────────────────────────┐
│ TCC 模式 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Try:预留资源(不真正扣减) │
│ A: UPDATE orders SET status='pending' ← 预占订单 │
│ B: Redis: DECRBY stock_pending 1 ← 预占库存 │
│ C: UPDATE accounts SET frozen=frozen+100 ← 冻结余额 │
│ │
│ Confirm:确认执行(真正扣减) │
│ A: UPDATE orders SET status='confirmed' │
│ B: Redis: DECRBY stock 1; DECRBY stock_pending 1 │
│ C: UPDATE accounts SET balance=balance-100, frozen=frozen-100│
│ │
│ Cancel:取消执行(回滚预留) │
│ A: DELETE orders WHERE status='pending' │
│ B: Redis: INCRBY stock_pending 1 │
│ C: UPDATE accounts SET frozen=frozen-100 │
│ │
│ 优点: │
│ ✅ 不用锁定资源(Try 阶段只是预留) │
│ ✅ 异步执行 Confirm/Cancel │
│ │
│ 缺点: │
│ ❌ 业务侵入性强:每个业务都要写 Try/Confirm/Cancel │
│ ❌ 空回滚:Try 失败了,但 Cancel 仍被执行 │
│ ❌ 悬挂:Confirm 超时失败,但实际执行成功了 │
│ │
└─────────────────────────────────────────────────────────────┘方案三:Saga 模式(异步补偿)
┌─────────────────────────────────────────────────────────────┐
│ Saga 模式(编排型) │
├─────────────────────────────────────────────────────────────┤
│ │
│ 适用场景:业务流程长,涉及多个服务,无法锁定资源 │
│ │
│ Saga 编排器: │
│ │
│ Step 1: 扣库存(服务 A) │
│ Step 2: 创建订单(服务 B)← 成功了 │
│ Step 3: 扣余额(服务 C)← 失败了! │
│ │
│ 补偿: │
│ Step 4: 取消订单(服务 B 回滚) │
│ Step 5: 恢复库存(服务 A 回滚) │
│ │
│ Saga vs TCC: │
│ • TCC:同步两阶段,Try 阶段锁定资源 │
│ • Saga:异步多阶段,每个步骤都可以正向或逆向 │
│ │
└─────────────────────────────────────────────────────────────┘方案四:本地消息表(最实用)
┌─────────────────────────────────────────────────────────────┐
│ 本地消息表方案 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 核心思路:把消息先存在本地数据库,用数据库事务保证原子性 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ BEGIN TRANSACTION │ │
│ │ INSERT INTO orders (...) VALUES (...) │ │
│ │ INSERT INTO outbox (id, topic, payload, status) │ │
│ │ VALUES (1, 'stock', '{"product_id":1}', 'pending')│ │
│ │ COMMIT │ │
│ │ │ │
│ │ -- 订单和消息条目在一个事务中,要麼一起成功 │ │
│ │ -- 要麼一起失败 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 后台任务:扫描 outbox 表,发送到 MQ,标记 status='sent' │
│ │
│ 优点: │
│ ✅ 实现简单,不依赖分布式事务 │
│ ✅ 数据库事务保证原子性 │
│ ✅ 可结合 Canal 监听 binlog 更优雅 │
│ │
│ 缺点: │
│ ❌ 有短暂不一致(消息发送有延迟) │
│ ❌ 需要额外的定时任务扫描 outbox │
│ │
└─────────────────────────────────────────────────────────────┘升华:分布式事务选型
┌─────────────────────────────────────────────────────────────┐
│ 分布式事务选型决策树 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 业务场景: │
│ ├── 扣库存 + 下订单(简单两方事务) │
│ │ └── 本地消息表(最简单,推荐) │
│ │ │
│ ├── 涉及支付/金融(强一致性要求) │
│ │ └── RocketMQ 事务消息 / Seata AT 模式 │
│ │ │
│ ├── 业务流程长,涉及 3+ 服务(长流程) │
│ │ └── Saga 编排 / Seata Saga 模式 │
│ │ │
│ └── 需要锁定资源(高并发库存抢券) │
│ └── TCC / Seata XA(但性能差) │
│ │
│ 一句话总结: │
│ 大部分互联网场景,本地消息表是成本最低、效果最好的方案。 │
│ │
└─────────────────────────────────────────────────────────────┘"AI 可查 vs 必须理解"清单
AI 可查:
✅ Seata / Saga 框架的具体配置和 API
✅ 2PC / 3PC 的协议细节
✅ UidGenerator / Leaf 的部署配置
必须理解:
🔴 CAP 定理和 BASE 理论的核心区别
🔴 Raft 选主的基本流程(不需要背细节)
🔴 为什么 2PC 不适合高并发场景
🔴 TCC / Saga / 本地消息表的适用场景
🔴 分布式 ID 的几种方案和各自优缺点学习状态:🟡 开始学习