分布式系统从零到精通验收清单
这页用来验收你是否真正学懂分布式系统。不是会背 CAP、BASE、TCC、Saga、Seata、Redisson 就算会,而是要能解释:为什么单机里简单的本地调用和本地事务,拆到多服务后会变成超时、重复、不一致、补偿、对账和排查问题。
分布式学习主线是一句话:
网络不可靠,状态会分散,一致性有代价;所以分布式系统必须有超时、重试、幂等、状态机、补偿、对账、限流、熔断、锁、事务、消息和可观测性。
最终目标
| 能力 | 达标标准 |
|---|---|
| 零基础能懂 | 能从“为什么一个本地方法变成远程调用后会不确定”讲起 |
| 原理能讲 | 能解释 CAP/BASE、强一致、最终一致、幂等、分布式事务、分布式锁 |
| Demo 能写 | 能写幂等、状态机、本地消息表、Redis 锁、限流的最小 Demo |
| 项目能落地 | 能放到订单、支付、库存、采集、ES 同步、缓存一致性场景 |
| 故障能排查 | 能排查超时、重复、消息堆积、ES 更新失败、缓存脏读、锁误删 |
| 面试能闭环 | 面试页标准回答,知识点页讲清原理、失败边界和选型 |
总路线
flowchart TD
A["单机到分布式"] --> B["远程调用不确定性"]
B --> C["超时、重试、查询确认"]
C --> D["幂等和状态机"]
D --> E["CAP、BASE、一致性模型"]
E --> F["分布式事务选型"]
F --> G["分布式锁和 Fencing Token"]
G --> H["缓存、MQ、ES 最终一致"]
H --> I["限流、熔断、降级"]
I --> J["可观测、补偿、对账"]阶段 1:单机问题为什么到分布式会变难
单体应用里,下单可能只是一段本地代码和一个数据库事务:
flowchart TD
A["下单请求"] --> B["订单模块"]
B --> C["库存模块"]
C --> D["本地数据库事务"]
D --> E["提交或回滚"]微服务拆分后,订单、库存、支付可能在不同服务和数据库:
flowchart TD
A["下单请求"] --> B["订单服务和订单库"]
B --> C["库存服务和库存库"]
C --> D["支付服务和支付库"]
D --> E{"任意一步可能超时、失败、重复"}单机和分布式的关键差异:
| 问题 | 单机 | 分布式 |
|---|---|---|
| 调用 | 本地方法,结果明确 | 远程调用,可能超时且状态未知 |
| 事务 | 一个数据库事务 | 多个服务、多个库、多个资源 |
| 锁 | JVM 锁或数据库锁 | 多实例之间需要分布式协调 |
| 日志 | 一个进程里看日志 | 多服务日志需要 traceId 串联 |
| 失败 | 多数可直接抛异常回滚 | 需要补偿、重试、对账 |
详细知识点:分布式系统基础。
阶段 2:远程调用的三种不确定状态
远程调用最麻烦的不是明确成功或明确失败,而是超时。
flowchart TD
A["订单服务发送创建支付单请求"] --> B["支付服务可能未执行、执行中或已成功"]
B --> C["响应超时或丢失"]
C --> D["订单服务处于结果未知"]
D --> E["幂等重试、查询确认或补偿"]调用方看到超时,真实情况可能是:
| 调用方看到 | 下游真实状态 | 正确处理 |
|---|---|---|
| 超时 | 下游没收到请求 | 可以安全重试 |
| 超时 | 下游执行成功但响应丢失 | 不能重复执行,应该查询确认 |
| 超时 | 下游执行中 | 需要幂等、查询、补偿 |
所以远程写操作不能只靠重试,要有:
- 业务请求号。
- 幂等键。
- 查询接口。
- 状态机。
- 超时和退避。
- 补偿任务。
- 对账机制。
Demo:带幂等键的远程调用
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
public class PaymentClient {
public String createPayment(String orderNo) throws Exception {
String requestId = UUID.randomUUID().toString();
String idempotentKey = "pay:" + orderNo;
String body = "{\"orderNo\":\"" + orderNo + "\"}";
HttpURLConnection connection = (HttpURLConnection) new URL(
"http://payment-service/payments").openConnection();
connection.setRequestMethod("POST");
connection.setConnectTimeout(2000);
connection.setReadTimeout(3000);
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("X-Request-Id", requestId);
connection.setRequestProperty("Idempotent-Key", idempotentKey);
try (OutputStream output = connection.getOutputStream()) {
output.write(body.getBytes(StandardCharsets.UTF_8));
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(
connection.getInputStream(), StandardCharsets.UTF_8))) {
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) response.append(line);
return response.toString();
} finally {
connection.disconnect();
}
}
}这只是最小防线。生产还要配合熔断、限流、日志、traceId、查询确认和补偿。
该版本兼容JDK 8;Java 11+/17+可改用标准HttpClient,但协议失败窗口并不会因API更新而消失。
阶段 3:幂等是分布式写操作的地基
幂等表示同一个业务请求执行一次和执行多次,最终业务结果一致。
常见重复来源:
- 用户重复点击。
- 网关重试。
- Feign 超时重试。
- MQ 至少投递一次。
- 支付回调多次通知。
- 补偿任务重复执行。
幂等方案
| 方案 | 适合场景 | 注意点 |
|---|---|---|
| 唯一索引 | 创建订单、发券流水 | 依赖数据库约束兜底 |
| 幂等表 | 第三方回调、补偿任务 | 先插入幂等记录再处理 |
| 状态机 | 订单、支付、退款 | SQL 带前置状态条件 |
| 乐观锁版本号 | 更新资源状态 | 更新失败要重查状态 |
| 分布式锁 | 热点互斥 | 不能替代幂等和事务 |
Demo:支付回调状态机
UPDATE orders
SET status = 'PAID',
paid_time = NOW()
WHERE order_no = ?
AND status = 'WAIT_PAY';如果重复回调第二次执行,影响行数为 0。此时不要报错,可以查询订单状态,如果已经是 PAID,说明是幂等成功。
详细知识点:幂等设计。
阶段 4:CAP、BASE 和一致性模型
CAP 不是简单“三选二”。更准确地说:
分布式系统必须面对网络分区 P;当分区发生时,只能在一致性 C 和可用性 A 之间取舍。
flowchart TD
A["网络分区发生"] --> B["节点之间无法通信"]
B --> C{"继续响应请求吗"}
C -->|继续响应| D["偏 AP<br/>可能短暂不一致"]
C -->|拒绝部分请求| E["偏 CP<br/>保护一致性"]| 概念 | 含义 | 例子 |
|---|---|---|
| C | 分区时仍保证读写看到一致结果 | ZooKeeper 少数派不可写 |
| A | 每个非故障节点都能响应请求 | Eureka 保留注册表 |
| P | 网络分区,节点互相不可达 | 机房网络故障、防火墙、路由异常 |
BASE 是对强一致的工程折中:
| BASE | 解释 | 工程要求 |
|---|---|---|
| Basically Available | 基本可用 | 非核心能力降级 |
| Soft State | 软状态 | 允许中间状态可观察 |
| Eventually Consistent | 最终一致 | 重试、补偿、对账、告警 |
BASE 不是“不管一致性”。如果没有补偿和对账,只是把不一致藏起来。
详细知识点:CAP、BASE 与一致性理论。
阶段 5:分布式事务不是只有 Seata
分布式事务解决的是一次业务操作跨多个服务、多个数据库、多个资源时,如何保证最终结果正确。
方案地图
flowchart TD
A["分布式事务"] --> B["强一致"]
A --> C["最终一致"]
B --> D["XA/JTA/2PC"]
B --> E["Seata AT/XA"]
C --> F["TCC"]
C --> G["Saga"]
C --> H["本地消息表"]
C --> I["MQ 事务消息"]
C --> J["最大努力通知"]选型表
| 方案 | 适合场景 | 代价 |
|---|---|---|
| XA/JTA/2PC | 短事务、强一致、多资源支持 XA | 锁时间长、性能低、恢复复杂 |
| TCC | 资源可预留,如库存、余额、券 | 业务侵入大,要处理空回滚和悬挂 |
| Saga | 订单履约、审批、物流长流程 | 补偿复杂,只能最终一致 |
| 本地消息表 | 订单后置通知、积分、ES 同步 | 需要扫描、重试、死信、对账 |
| MQ 事务消息 | 本地事务和发消息一致 | 依赖 MQ 能力,消费端仍要幂等 |
| Seata | 框架化协调多种模式 | 不是银弹,要理解 AT/TCC/Saga/XA 边界 |
本地消息表 Demo
BEGIN;
UPDATE orders
SET status = 'PAID'
WHERE order_no = ?
AND status = 'WAIT_PAY';
INSERT INTO outbox_message(message_id, topic, biz_key, body, status)
VALUES (?, 'order.paid', ?, ?, 'NEW');
COMMIT;后台任务扫描 NEW 消息发送 MQ,发送成功改成 SENT,失败重试,超过阈值进入异常表。下游消费必须幂等。
详细知识点:分布式事务总览、分布式事务选型与排查。
阶段 6:TCC、Saga 和 XA 的失败边界
TCC 必须处理的问题
| 问题 | 含义 | 解决 |
|---|---|---|
| 幂等 | Confirm/Cancel 可能重复 | 分支事务表记录状态 |
| 空回滚 | Try 没执行,Cancel 先到了 | Cancel 发现无 Try 记录则插入取消标记 |
| 悬挂 | Cancel 后 Try 又到达 | Try 发现已取消则拒绝 |
| 超时释放 | Try 预留资源后长时间无结果 | 定时任务释放或事务协调器回滚 |
Saga 必须处理的问题
Saga 每一步都是本地提交,失败后做补偿。补偿不是数据库回滚,而是业务上的反向动作:
- 订单已创建,补偿为取消订单。
- 库存已扣减,补偿为加回库存。
- 优惠券已发放,补偿为作废券。
补偿动作也可能失败,所以 Saga 必须有重试、幂等、人工处理和对账。
XA 为什么性能低
XA/2PC 需要多个资源先 prepare 并锁住相关数据,等协调者统一 commit 或 rollback。参与者慢、网络抖动、协调者异常都会延长锁持有时间。
详细知识点:XA/JTA/2PC、TCC、Saga。
阶段 7:分布式锁能做什么,不能做什么
分布式锁解决的是多实例之间的短时间互斥,例如:
- 热点缓存重建。
- 多实例定时任务防重复。
- 低频资源互斥操作。
- 防止同一订单被多个节点同时处理。
它不能替代:
- 数据库事务。
- 唯一索引。
- 状态机。
- 幂等表。
- 补偿和对账。
Redis 锁正确释放
Redis 锁必须使用唯一 token,释放时校验 token,避免误删别人的锁。
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
endFencing Token 为什么重要
如果客户端拿到锁后发生长时间 GC,锁过期被别人拿走,旧客户端醒来继续写数据,就可能覆盖新客户端结果。Fencing Token 是递增版本号,下游只接受更大的 token。
flowchart TD
A["客户端A拿到 token=1"] --> B["A 长时间停顿"]
B --> C["锁过期"]
C --> D["客户端B拿到 token=2"]
D --> E["下游接受 token=2 写入"]
B --> F["A 醒来带 token=1 写入"]
F --> G["下游拒绝旧 token"]详细知识点:Redisson 分布式锁、Redis 红锁。
阶段 8:缓存、MQ、ES 的最终一致
Redis 和数据库一致性
常见 Cache Aside:
- 读缓存。
- 缓存未命中读数据库。
- 写入缓存并设置 TTL。
- 更新时先更新数据库。
- 事务提交后删除缓存。
- 删除失败进入重试和告警。
缓存不是事实源,数据库才是。关键交易状态不能只信缓存。
MySQL 和 ES 一致性
MySQL 是事实源,ES 是搜索视图。常见方案:
- 本地消息表 + MQ 同步 ES。
- Binlog CDC 同步 ES。
- 定时补偿任务。
- 失败进入死信。
- 严重不一致时按 MySQL 重建索引并用别名切换。
如果 ES 更新失败,不能静默丢弃。要记录失败事件、重试、死信、告警,并支持按业务主键重建文档。
MQ 最终一致
MQ 不能只看“消息发送成功”。完整链路包括:
flowchart TD
A["本地事务写业务数据"] --> B["记录消息或发送事务消息"]
B --> C["Broker 持久化"]
C --> D["消费者拉取或推送"]
D --> E["消费者幂等处理"]
E --> F["失败重试"]
F --> G["死信、补偿、对账"]详细知识点:缓存一致性、MySQL 与 ES 一致性、消息队列。
阶段 9:限流、熔断、降级和容量保护
| 能力 | 保护对象 | 典型场景 |
|---|---|---|
| 限流 | 保护自己不被入口流量打爆 | 秒杀、热点接口、采集任务 |
| 熔断 | 保护上游不被慢下游拖死 | 医院接口慢、支付服务异常 |
| 降级 | 非核心能力失败时兜底 | 推荐、通知、统计 |
| 隔离 | 防止一个依赖占满所有资源 | 线程池隔离、连接池隔离 |
| 队列削峰 | 把瞬时流量变成可消费速度 | 下单、采集入库、日志处理 |
分布式限流 Demo 思路
Redis 固定窗口限流最小思路:
String key = "rate:tenant:" + tenantId + ":" + currentSecond;
Long count = redis.incr(key);
if (count == 1) {
redis.expire(key, 2);
}
if (count > 100) {
throw new RuntimeException("too many requests");
}固定窗口简单,但边界处可能抖动;生产更常用滑动窗口、令牌桶或 Sentinel。
详细知识点:分布式限流。
阶段 10:共识、Dubbo、ZooKeeper 和协调系统
Raft 实现层必须说清什么
- Term、Vote、Entries 为什么必须先持久化再发送 VoteResp/AppendResp?
- PreVote 与 CheckQuorum 分别解决什么故障窗口?
- Leader 为什么追加当前 Term 空条目?
- Progress 的 Probe、Replicate、Snapshot 和 Inflights 怎样工作?
- 为什么 Leader 只能按多数计数直接提交当前 Term Entry?
- ReadIndex 为什么既要 Quorum 确认,又要等待 AppliedIndex?
- Snapshot 的数据、Index、Term、ConfState 为什么必须原子一致?
- Joint Consensus 为什么同时要求旧、新两个 Quorum?
- 新成员为什么先做 Learner,追平后再 Promote?
- 客户端超时为什么仍要 requestId 去重?
Dubbo 解决什么
Dubbo 是 RPC 框架,核心是让 Consumer 像调用本地接口一样调用远程 Provider。但它本质仍是网络调用,必须考虑超时、重试、幂等、负载均衡、注册发现、版本兼容。
ZooKeeper 解决什么
ZooKeeper 是协调组件,适合低频、关键、强协调元数据:
- 注册发现。
- 配置通知。
- 分布式锁。
- 选主。
- 集群成员管理。
ZK 不适合存大量业务数据,也不适合高频读写。
ZK 锁原理
flowchart TD
A["多个客户端创建临时顺序节点"] --> B["序号最小者获得锁"]
B --> C["其他客户端监听前一个节点"]
C --> D["前一个节点删除"]
D --> E["再次判断自己是否最小"]
E --> F["获得锁或继续等待"]详细知识点:共识算法主线 · Raft内部原理与生产治理 · 共识与Raft独立面试题 · Dubbo · ZooKeeper。
阶段 11:Service Mesh 与基础设施流量治理
必须说清的配置链与请求链
- Istiod 怎样从 Service、EndpointSlice 和策略生成每个代理不同的 LDS/RDS/CDS/EDS/SDS?
- xDS 的 version、nonce、ACK、NACK、SotW、Delta 和 ADS 分别解决什么?
- Listener/Cluster Warming 为什么能避免半初始化配置直接接流量?
- 一个请求怎样依次经过 Listener、Filter Chain、HCM、Route、Cluster、Endpoint 和连接池?
- HTTP/2 长连接为什么会让扩容、摘流和灰度短暂不均?
- Priority、Locality、Panic Mode、Outlier Detection 与 LB 怎样共同选择 Endpoint?
- Envoy Circuit Breaker、Resilience4j 断路器、Overload Manager 和业务降级有什么区别?
- SDS 轮换后为什么新连接与旧连接可能使用不同证书?
- Sidecar 与 Ambient 的 ztunnel、HBONE、Waypoint 各负责哪一层?
NR/UH/UF/UO/UT/URX应按什么顺序取证?
如果只能回答“Sidecar 拦截流量、控制面下发配置”,说明还没有达到生产级。完整学习:Service Mesh 主线 · 内部原理与生产治理 · 独立面试题。
阶段 12:商业场景验收
场景 1:支付成功改订单并同步积分、短信、ES
推荐链路:
flowchart TD
A["支付回调"] --> B["验签和金额校验"]
B --> C["幂等更新订单 PAID"]
C --> D["本地消息表写 order.paid"]
D --> E["事务提交"]
E --> F["异步投递 MQ"]
F --> G["积分服务幂等消费"]
F --> H["通知服务幂等消费"]
F --> I["ES 同步服务幂等消费"]不能把积分、短信、ES 更新都塞进支付回调本地事务里,否则任何下游慢都会拖住核心支付链路。
场景 2:医疗采集任务
要求:
taskId、hospitalCode、batchNo做幂等。- 医院接口设置超时和 traceId。
- 采集结果分批入库。
- 异步清洗和入资产库。
- 失败批次进入补偿任务。
- ES 同步失败可重试和重建。
验收问题:
- 医院接口超时后怎么确认真实状态?
- 重复采集同一批次如何防止重复入库?
- 采集服务扩容后为什么可能仍然堆积?
场景 3:库存扣减
库存扣减常见设计:
- 数据库行条件更新防超卖。
- 请求幂等号防重复扣减。
- 状态机控制订单状态。
- 高并发时 Redis 预扣或队列削峰。
- 最终以数据库库存和流水为准。
阶段 12:生产排查
分布式问题排查总图
flowchart TD
A["发现问题"] --> B{"表现是什么"}
B --> C["接口慢或超时"]
B --> D["数据不一致"]
B --> E["消息堆积"]
B --> F["重复执行"]
C --> G["查 traceId、线程池、连接池、下游"]
D --> H["查事实源、消息、补偿、对账"]
E --> I["查生产消费 TPS、Lag 分布、慢消息、下游"]
F --> J["查幂等键、唯一索引、状态机"]高频故障
| 故障 | 排查重点 |
|---|---|
| 超时 | 下游是否执行、连接池、线程池、慢 SQL、GC |
| 重复扣款 | 幂等表、唯一索引、状态机、支付流水 |
| MQ 堆积 | 生产/消费 TPS、Lag 分布、慢消息、热点 key、重试风暴 |
| ES 数据旧 | MQ/CDC 状态、失败重试、死信、补偿、索引重建 |
| 缓存脏读 | 删除缓存是否失败、TTL、延迟双删、读写路径 |
| Redis 锁误删 | token 校验、锁过期、GC 暂停、Lua 释放 |
| 分布式事务卡住 | 全局事务状态、分支事务、undo_log、锁、补偿 |
| 注册发现异常 | 注册中心、客户端缓存、健康状态、网络分区 |
阶段 13:面试闭环
flowchart TD
A["面试题"] --> B["先讲问题本质"]
B --> C["再讲方案取舍"]
C --> D["补失败边界"]
D --> E["落到项目场景"]
E --> F["说排查闭环"]| 面试题 | 必须答到的深度 |
|---|---|
| 分布式为什么复杂 | 网络不可靠、状态分散、一致性有代价 |
| 超时后怎么处理 | 不等于失败,查询确认、幂等、补偿 |
| CAP 怎么理解 | P 必须面对,分区时 C/A 取舍 |
| BASE 是什么 | 允许中间状态,但必须最终修正 |
| 幂等怎么做 | 唯一索引、幂等表、状态机、乐观锁 |
| 分布式事务怎么选 | XA、TCC、Saga、消息、Seata 按业务风险选 |
| 分布式锁能做什么 | 互斥,不替代事务、幂等、状态机 |
| 缓存一致性 | DB 事实源,更新 DB 后删缓存,失败补偿 |
| ES 一致性 | MySQL 事实源,MQ/CDC 同步,失败重试重建 |
| 消息堆积 | TPS、Lag 分布、慢消息、下游瓶颈 |
面试标准回答看:分布式面试题。
最终验收题
- 为什么远程调用超时不等于失败?
- 写接口为什么必须设计幂等?
- 幂等表、唯一索引、状态机分别适合什么场景?
- CAP 为什么不是简单三选二?
- P 为什么不是分库分表?
- BASE 为什么不是不管一致性?
- 强一致和最终一致怎么选?
- XA/JTA/2PC 为什么性能低?
- TCC 为什么要处理空回滚和悬挂?
- Saga 的补偿为什么也要幂等?
- 本地消息表如何保证本地事务和消息一致?
- MQ 事务消息解决什么,不解决什么?
- Seata AT、TCC、Saga、XA 各自边界是什么?
- Redis 锁为什么要 token 校验释放?
- Redlock 是什么?为什么仍要业务兜底?
- Fencing Token 解决什么问题?
- Redis 和数据库一致性怎么做?
- ES 更新失败怎么办?
- 消息堆积先看哪些指标?
- 扩容消费者短暂有效后又堆积是什么原因?
- 限流、熔断、降级区别是什么?
- ZooKeeper 为什么适合协调,不适合存业务数据?
- Dubbo 调用为什么仍然要考虑超时和幂等?
- Raft 的 VoteResp 和 AppendResp 为什么必须晚于持久化?
- 旧 Leader 的未提交日志为什么允许被覆盖?
- ReadIndex 返回后为什么还不能立即读状态机?
- 直接把空节点加入 Voter 为什么可能让集群失去 Quorum?
- xDS ACK 为什么不等于全网真实流量已切换?
- Pod 已从 EndpointSlice 删除后为什么仍可能收到请求?
- 多数 Endpoint 不健康时 Envoy 为什么还可能进入 Panic Mode 发流量?
- 医疗采集任务如何设计幂等和补偿?
- 分布式线上数据不一致怎么排查?
本章小结
分布式系统不是靠某个框架自动可靠,而是靠一整套工程闭环可靠:超时、重试、查询确认、幂等、状态机、消息、事务、补偿、对账、锁、限流、熔断、日志、指标、链路追踪。学懂这些,再看 Spring Cloud、Dubbo、ZooKeeper、RocketMQ、Redis、Seata、ES 同步,才能真正知道为什么这样设计,不这样会怎样。
