Skip to content

分布式系统从零到精通验收清单

这页用来验收你是否真正学懂分布式系统。不是会背 CAP、BASE、TCC、Saga、Seata、Redisson 就算会,而是要能解释:为什么单机里简单的本地调用和本地事务,拆到多服务后会变成超时、重复、不一致、补偿、对账和排查问题。

分布式学习主线是一句话:

网络不可靠,状态会分散,一致性有代价;所以分布式系统必须有超时、重试、幂等、状态机、补偿、对账、限流、熔断、锁、事务、消息和可观测性。

最终目标

能力达标标准
零基础能懂能从“为什么一个本地方法变成远程调用后会不确定”讲起
原理能讲能解释 CAP/BASE、强一致、最终一致、幂等、分布式事务、分布式锁
Demo 能写能写幂等、状态机、本地消息表、Redis 锁、限流的最小 Demo
项目能落地能放到订单、支付、库存、采集、ES 同步、缓存一致性场景
故障能排查能排查超时、重复、消息堆积、ES 更新失败、缓存脏读、锁误删
面试能闭环面试页标准回答,知识点页讲清原理、失败边界和选型

总路线

mermaid
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:单机问题为什么到分布式会变难

单体应用里,下单可能只是一段本地代码和一个数据库事务:

mermaid
flowchart TD
    A["下单请求"] --> B["订单模块"]
    B --> C["库存模块"]
    C --> D["本地数据库事务"]
    D --> E["提交或回滚"]

微服务拆分后,订单、库存、支付可能在不同服务和数据库:

mermaid
flowchart TD
    A["下单请求"] --> B["订单服务和订单库"]
    B --> C["库存服务和库存库"]
    C --> D["支付服务和支付库"]
    D --> E{"任意一步可能超时、失败、重复"}

单机和分布式的关键差异:

问题单机分布式
调用本地方法,结果明确远程调用,可能超时且状态未知
事务一个数据库事务多个服务、多个库、多个资源
JVM 锁或数据库锁多实例之间需要分布式协调
日志一个进程里看日志多服务日志需要 traceId 串联
失败多数可直接抛异常回滚需要补偿、重试、对账

详细知识点:分布式系统基础

阶段 2:远程调用的三种不确定状态

远程调用最麻烦的不是明确成功或明确失败,而是超时。

mermaid
flowchart TD
    A["订单服务发送创建支付单请求"] --> B["支付服务可能未执行、执行中或已成功"]
    B --> C["响应超时或丢失"]
    C --> D["订单服务处于结果未知"]
    D --> E["幂等重试、查询确认或补偿"]

调用方看到超时,真实情况可能是:

调用方看到下游真实状态正确处理
超时下游没收到请求可以安全重试
超时下游执行成功但响应丢失不能重复执行,应该查询确认
超时下游执行中需要幂等、查询、补偿

所以远程写操作不能只靠重试,要有:

  • 业务请求号。
  • 幂等键。
  • 查询接口。
  • 状态机。
  • 超时和退避。
  • 补偿任务。
  • 对账机制。

Demo:带幂等键的远程调用

java
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:支付回调状态机

sql
UPDATE orders
SET status = 'PAID',
    paid_time = NOW()
WHERE order_no = ?
  AND status = 'WAIT_PAY';

如果重复回调第二次执行,影响行数为 0。此时不要报错,可以查询订单状态,如果已经是 PAID,说明是幂等成功。

详细知识点:幂等设计

阶段 4:CAP、BASE 和一致性模型

CAP 不是简单“三选二”。更准确地说:

分布式系统必须面对网络分区 P;当分区发生时,只能在一致性 C 和可用性 A 之间取舍。

mermaid
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

分布式事务解决的是一次业务操作跨多个服务、多个数据库、多个资源时,如何保证最终结果正确。

方案地图

mermaid
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

sql
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/2PCTCCSaga

阶段 7:分布式锁能做什么,不能做什么

分布式锁解决的是多实例之间的短时间互斥,例如:

  • 热点缓存重建。
  • 多实例定时任务防重复。
  • 低频资源互斥操作。
  • 防止同一订单被多个节点同时处理。

它不能替代:

  • 数据库事务。
  • 唯一索引。
  • 状态机。
  • 幂等表。
  • 补偿和对账。

Redis 锁正确释放

Redis 锁必须使用唯一 token,释放时校验 token,避免误删别人的锁。

lua
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

Fencing Token 为什么重要

如果客户端拿到锁后发生长时间 GC,锁过期被别人拿走,旧客户端醒来继续写数据,就可能覆盖新客户端结果。Fencing Token 是递增版本号,下游只接受更大的 token。

mermaid
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:

  1. 读缓存。
  2. 缓存未命中读数据库。
  3. 写入缓存并设置 TTL。
  4. 更新时先更新数据库。
  5. 事务提交后删除缓存。
  6. 删除失败进入重试和告警。

缓存不是事实源,数据库才是。关键交易状态不能只信缓存。

MySQL 和 ES 一致性

MySQL 是事实源,ES 是搜索视图。常见方案:

  • 本地消息表 + MQ 同步 ES。
  • Binlog CDC 同步 ES。
  • 定时补偿任务。
  • 失败进入死信。
  • 严重不一致时按 MySQL 重建索引并用别名切换。

如果 ES 更新失败,不能静默丢弃。要记录失败事件、重试、死信、告警,并支持按业务主键重建文档。

MQ 最终一致

MQ 不能只看“消息发送成功”。完整链路包括:

mermaid
flowchart TD
    A["本地事务写业务数据"] --> B["记录消息或发送事务消息"]
    B --> C["Broker 持久化"]
    C --> D["消费者拉取或推送"]
    D --> E["消费者幂等处理"]
    E --> F["失败重试"]
    F --> G["死信、补偿、对账"]

详细知识点:缓存一致性MySQL 与 ES 一致性消息队列

阶段 9:限流、熔断、降级和容量保护

能力保护对象典型场景
限流保护自己不被入口流量打爆秒杀、热点接口、采集任务
熔断保护上游不被慢下游拖死医院接口慢、支付服务异常
降级非核心能力失败时兜底推荐、通知、统计
隔离防止一个依赖占满所有资源线程池隔离、连接池隔离
队列削峰把瞬时流量变成可消费速度下单、采集入库、日志处理

分布式限流 Demo 思路

Redis 固定窗口限流最小思路:

java
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 实现层必须说清什么

  1. Term、Vote、Entries 为什么必须先持久化再发送 VoteResp/AppendResp?
  2. PreVote 与 CheckQuorum 分别解决什么故障窗口?
  3. Leader 为什么追加当前 Term 空条目?
  4. Progress 的 Probe、Replicate、Snapshot 和 Inflights 怎样工作?
  5. 为什么 Leader 只能按多数计数直接提交当前 Term Entry?
  6. ReadIndex 为什么既要 Quorum 确认,又要等待 AppliedIndex?
  7. Snapshot 的数据、Index、Term、ConfState 为什么必须原子一致?
  8. Joint Consensus 为什么同时要求旧、新两个 Quorum?
  9. 新成员为什么先做 Learner,追平后再 Promote?
  10. 客户端超时为什么仍要 requestId 去重?

Dubbo 解决什么

Dubbo 是 RPC 框架,核心是让 Consumer 像调用本地接口一样调用远程 Provider。但它本质仍是网络调用,必须考虑超时、重试、幂等、负载均衡、注册发现、版本兼容。

ZooKeeper 解决什么

ZooKeeper 是协调组件,适合低频、关键、强协调元数据:

  • 注册发现。
  • 配置通知。
  • 分布式锁。
  • 选主。
  • 集群成员管理。

ZK 不适合存大量业务数据,也不适合高频读写。

ZK 锁原理

mermaid
flowchart TD
    A["多个客户端创建临时顺序节点"] --> B["序号最小者获得锁"]
    B --> C["其他客户端监听前一个节点"]
    C --> D["前一个节点删除"]
    D --> E["再次判断自己是否最小"]
    E --> F["获得锁或继续等待"]

详细知识点:共识算法主线 · Raft内部原理与生产治理 · 共识与Raft独立面试题 · Dubbo · ZooKeeper

阶段 11:Service Mesh 与基础设施流量治理

必须说清的配置链与请求链

  1. Istiod 怎样从 Service、EndpointSlice 和策略生成每个代理不同的 LDS/RDS/CDS/EDS/SDS?
  2. xDS 的 version、nonce、ACK、NACK、SotW、Delta 和 ADS 分别解决什么?
  3. Listener/Cluster Warming 为什么能避免半初始化配置直接接流量?
  4. 一个请求怎样依次经过 Listener、Filter Chain、HCM、Route、Cluster、Endpoint 和连接池?
  5. HTTP/2 长连接为什么会让扩容、摘流和灰度短暂不均?
  6. Priority、Locality、Panic Mode、Outlier Detection 与 LB 怎样共同选择 Endpoint?
  7. Envoy Circuit Breaker、Resilience4j 断路器、Overload Manager 和业务降级有什么区别?
  8. SDS 轮换后为什么新连接与旧连接可能使用不同证书?
  9. Sidecar 与 Ambient 的 ztunnel、HBONE、Waypoint 各负责哪一层?
  10. NR/UH/UF/UO/UT/URX 应按什么顺序取证?

如果只能回答“Sidecar 拦截流量、控制面下发配置”,说明还没有达到生产级。完整学习:Service Mesh 主线 · 内部原理与生产治理 · 独立面试题

阶段 12:商业场景验收

场景 1:支付成功改订单并同步积分、短信、ES

推荐链路:

mermaid
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:医疗采集任务

要求:

  • taskIdhospitalCodebatchNo 做幂等。
  • 医院接口设置超时和 traceId。
  • 采集结果分批入库。
  • 异步清洗和入资产库。
  • 失败批次进入补偿任务。
  • ES 同步失败可重试和重建。

验收问题:

  • 医院接口超时后怎么确认真实状态?
  • 重复采集同一批次如何防止重复入库?
  • 采集服务扩容后为什么可能仍然堆积?

场景 3:库存扣减

库存扣减常见设计:

  • 数据库行条件更新防超卖。
  • 请求幂等号防重复扣减。
  • 状态机控制订单状态。
  • 高并发时 Redis 预扣或队列削峰。
  • 最终以数据库库存和流水为准。

阶段 12:生产排查

分布式问题排查总图

mermaid
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:面试闭环

mermaid
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 分布、慢消息、下游瓶颈

面试标准回答看:分布式面试题

最终验收题

  1. 为什么远程调用超时不等于失败?
  2. 写接口为什么必须设计幂等?
  3. 幂等表、唯一索引、状态机分别适合什么场景?
  4. CAP 为什么不是简单三选二?
  5. P 为什么不是分库分表?
  6. BASE 为什么不是不管一致性?
  7. 强一致和最终一致怎么选?
  8. XA/JTA/2PC 为什么性能低?
  9. TCC 为什么要处理空回滚和悬挂?
  10. Saga 的补偿为什么也要幂等?
  11. 本地消息表如何保证本地事务和消息一致?
  12. MQ 事务消息解决什么,不解决什么?
  13. Seata AT、TCC、Saga、XA 各自边界是什么?
  14. Redis 锁为什么要 token 校验释放?
  15. Redlock 是什么?为什么仍要业务兜底?
  16. Fencing Token 解决什么问题?
  17. Redis 和数据库一致性怎么做?
  18. ES 更新失败怎么办?
  19. 消息堆积先看哪些指标?
  20. 扩容消费者短暂有效后又堆积是什么原因?
  21. 限流、熔断、降级区别是什么?
  22. ZooKeeper 为什么适合协调,不适合存业务数据?
  23. Dubbo 调用为什么仍然要考虑超时和幂等?
  24. Raft 的 VoteResp 和 AppendResp 为什么必须晚于持久化?
  25. 旧 Leader 的未提交日志为什么允许被覆盖?
  26. ReadIndex 返回后为什么还不能立即读状态机?
  27. 直接把空节点加入 Voter 为什么可能让集群失去 Quorum?
  28. xDS ACK 为什么不等于全网真实流量已切换?
  29. Pod 已从 EndpointSlice 删除后为什么仍可能收到请求?
  30. 多数 Endpoint 不健康时 Envoy 为什么还可能进入 Panic Mode 发流量?
  31. 医疗采集任务如何设计幂等和补偿?
  32. 分布式线上数据不一致怎么排查?

本章小结

分布式系统不是靠某个框架自动可靠,而是靠一整套工程闭环可靠:超时、重试、查询确认、幂等、状态机、消息、事务、补偿、对账、锁、限流、熔断、日志、指标、链路追踪。学懂这些,再看 Spring Cloud、Dubbo、ZooKeeper、RocketMQ、Redis、Seata、ES 同步,才能真正知道为什么这样设计,不这样会怎样。