分布式系统从零到生产级掌握
分布式不是“把项目拆成很多服务”这么简单。真正的分布式系统要面对网络失败、调用超时、重复请求、数据不一致、服务扩缩容、注册发现、限流熔断、链路追踪、补偿对账和线上排查。
一句话建立主线:
分布式系统 = 多个进程/服务/数据库通过网络协作完成一个业务目标;它用复杂度换容量、扩展性、容灾能力和团队协作效率。
如果只学框架名,会出现一种很危险的状态:知道 Nacos、Dubbo、Seata、RocketMQ、Redis 锁,但一问“超时后到底成功还是失败”“重复消费怎么保证不重复扣款”“CAP 为什么不是三选二”“扩容为什么没有效果”就答不上来。
如果你想检查自己是否真的从零基础学懂到能面试、能落地、能排查,按 分布式系统从零到精通验收清单 逐项验收。它把远程调用不确定性、CAP/BASE、幂等、事务、锁、缓存一致性、ES 同步、MQ、Dubbo、ZooKeeper、限流和生产排查拆成可检查的问题。
学习目标
学完这一页,你要能做到:
- 解释为什么单机问题拆到分布式后会变难。
- 解释远程调用为什么必须有超时、重试、幂等和熔断。
- 解释注册中心做什么,为什么它不转发业务流量。
- 解释 CAP、BASE、强一致、最终一致、读写一致的区别。
- 解释 Paxos、Raft、ZAB 这类共识/一致性协议解决什么问题。
- 解释分布式事务为什么不等于 Seata。
- 解释 XA/JTA/2PC、TCC、Saga、可靠消息、本地消息表、事务消息、Seata 的适用场景。
- 解释分布式 ID、幂等、限流、熔断、降级、分布式锁分别解决什么问题。
- 解释缓存一致性、ES 同步一致性、MQ 堆积和补偿对账怎么落地。
- 能按订单、支付、库存、医疗采集、搜索同步等商业场景设计分布式方案。
- 能从日志、指标、链路追踪、消息堆积、锁等待、数据库状态排查线上问题。
学习路线
flowchart TD
A["单体系统<br/>本地调用、本地事务"] --> B["远程调用<br/>网络、超时、重试"]
B --> C["服务治理<br/>注册发现、负载均衡"]
C --> D["可靠性保护<br/>限流、熔断、降级"]
D --> E["一致性理论<br/>CAP、BASE、PACELC"]
E --> F["数据一致性<br/>幂等、状态机、补偿、对账"]
F --> G["分布式事务<br/>XA、TCC、Saga、消息、Seata"]
G --> H["协调与锁<br/>ZooKeeper、Redis锁、Redlock"]
H --> I["生产排查<br/>日志、指标、Tracing、告警"]这条路线不能反过来。还没理解超时和幂等就学分布式事务,很容易把所有问题都交给框架;还没理解 CAP/BASE 就背 Seata 模式,会不知道为什么有些业务宁愿最终一致;还没理解可观测性,上线后只能靠猜。
第一步:为什么单机变分布式后会变难
单体应用里,下单可能是一个进程内调用:
flowchart TD
A["用户下单"] --> B["订单模块"]
B --> C["库存模块"]
C --> D["支付模块"]
D --> E["同一个数据库事务"]这种情况下,方法调用成功就是成功,异常就是失败,同一个数据库事务可以统一提交或回滚。
拆成分布式后:
flowchart TD
A["用户下单"] --> B["订单服务<br/>订单库"]
B --> C["库存服务<br/>库存库"]
C --> D["支付服务<br/>支付库"]
D --> E["优惠券服务<br/>优惠券库"]
C --> F{"网络超时"}
D --> G{"服务失败"}
E --> H{"状态不一致"}问题变多了:
| 问题 | 单机里 | 分布式里 |
|---|---|---|
| 方法调用 | 内存调用,失败明确 | 网络调用,可能超时、丢包、重复 |
| 事务 | 一个数据库事务 | 多个服务、多个数据库 |
| 锁 | JVM 锁或数据库锁 | 多实例,本地锁失效 |
| 状态 | 集中在一个库 | 分散到多个库和缓存 |
| 日志 | 看一个应用日志 | 多服务、多实例、多链路 |
| 扩容 | 加机器较简单 | 注册发现、负载均衡、数据一致性都受影响 |
所以分布式系统第一原则是:
不要假设远程调用一定成功,不要假设失败响应代表没有执行,不要假设重试不会造成重复结果。
第二步:远程调用的三种不确定状态
调用方请求下游服务,可能得到:
| 结果 | 含义 |
|---|---|
| 成功响应 | 下游明确执行成功 |
| 失败响应 | 下游明确拒绝或处理失败 |
| 超时/断连 | 调用方不知道下游到底有没有执行 |
最难的是第三种。
flowchart TD
A["订单服务发送创建支付单请求"] --> B["支付服务可能未收到、执行中或已提交"]
B --> C["响应超时或在网络中丢失"]
C --> D["订单服务只能确认没有按时拿到结果"]
D --> E["按业务流水查询事实并幂等恢复"]如果调用方看到超时就直接重试,而下游没有幂等,就可能创建两笔支付单。
因此远程调用最少需要:
| 防线 | 解决什么 |
|---|---|
| 超时 | 避免调用线程无限等待 |
| 重试 | 处理短暂网络抖动 |
| 幂等 | 重试不会造成重复业务结果 |
| 查询确认 | 超时后能查真实状态 |
| 熔断 | 下游持续异常时快速失败 |
| 降级 | 非核心能力失败不影响主流程 |
| traceId | 跨服务定位一次请求 |
第三步:远程调用基础 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, long amount) throws Exception {
String traceId = UUID.randomUUID().toString();
String idempotentKey = "payment:create:" + orderNo;
String body = String.format(
"{\"orderNo\":\"%s\",\"amount\":%d}", orderNo, amount);
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-Trace-Id", traceId);
connection.setRequestProperty("Idempotent-Key", idempotentKey);
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
try (OutputStream output = connection.getOutputStream()) {
output.write(bytes);
}
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();
}
}
}这段代码体现了三件事:
connectTimeout保护连接建立。readTimeout保护等待响应。Idempotent-Key让下游可以去重。
该Demo兼容JDK 8。Java 11+/17+可以使用java.net.http.HttpClient,但仍要保留连接超时、响应超时、幂等和结果确认。
真实项目还要配合 OpenFeign、Dubbo、Resilience4j、Sentinel、日志和链路追踪。
第四步:注册中心到底做什么
很多初学者误以为注册中心会转发请求,这是错误的。
注册中心的职责是保存服务地址,并通知调用方地址变化。
flowchart TD
A["Provider 启动"] --> B["向注册中心注册地址"]
C["Consumer 启动"] --> D["从注册中心订阅地址"]
D --> E["本地缓存 Provider 列表"]
E --> F["负载均衡选择一个 Provider"]
F --> G["Consumer 直接调用 Provider"]
B --> H["Provider 宕机或下线"]
H --> I["注册中心推送变更"]
I --> E注册中心不转发业务请求。真正业务流量通常是:
Consumer -> Provider不是:
Consumer -> 注册中心 -> Provider常见注册中心:
| 注册中心 | 特点 |
|---|---|
| Eureka | AP 思路,老 Spring Cloud 常见 |
| Nacos | 注册发现 + 配置中心,国内微服务常见 |
| ZooKeeper | 偏 CP,临时节点和 Watcher 适合协调 |
| Consul | 服务发现、健康检查、KV |
| etcd | Raft 共识,K8s 底层常用 |
注册中心挂了是否服务全挂?
- 如果 Consumer 已经缓存地址,短时间内可能还能调用。
- 新实例注册、下线感知、动态配置、治理规则会受影响。
- 如果本地缓存为空或地址全部失效,调用会失败。
所以服务治理不能只靠注册中心,还要有本地缓存、健康检查、超时、熔断和灰度策略。
第五步:负载均衡和扩容为什么不总有效
负载均衡是在多个可用实例中选择一个。
常见策略:
| 策略 | 原理 | 风险 |
|---|---|---|
| 轮询 | 逐个实例分配 | 不考虑机器差异 |
| 随机 | 随机选择 | 样本小时不均匀 |
| 权重 | 高配置实例更多流量 | 权重配置不当会倾斜 |
| 最少活跃 | 选当前请求少的实例 | 统计不准会误判 |
| 一致性 Hash | 同 key 尽量到同实例 | 热 key 会压垮单实例 |
扩容不总有效,原因可能是:
- 下游数据库已经是瓶颈。
- MQ 分区或队列数限制并行度。
- 热点 key 导致流量集中到少数实例。
- 服务有全局锁或单线程瓶颈。
- 注册中心推送或客户端缓存还没刷新。
- 扩容触发 Rebalance,短期反而抖动。
这也解释了 MQ 里“扩容消费者后短暂有效,后面又堆积”的问题:可能整体瓶颈不在消费者数量,而在分区并行度、下游写入、慢消息、热点 key 或重试风暴。
第六步:CAP 不是三选二
CAP:
| 字母 | 含义 |
|---|---|
| C | Consistency,一致性,读到最新正确数据 |
| A | Availability,可用性,每个请求都能得到非错误响应 |
| P | Partition Tolerance,分区容错,网络分区时系统仍能应对 |
最常见误解是“CAP 三选二”。更准确:
分布式系统必须考虑网络分区;当分区真的发生时,只能在一致性 C 和可用性 A 之间取舍。
flowchart TD
A["网络分区发生"] --> B["节点之间无法通信"]
B --> C{"继续对外服务吗"}
C -- "继续服务" --> D["可用性更好<br/>可能读到旧数据"]
C -- "拒绝部分请求" --> E["一致性更安全<br/>可用性下降"]举例:
| 系统 | 倾向 | 原因 |
|---|---|---|
| ZooKeeper | CP | 少数派不可写,保证一致性 |
| Eureka | AP | 注册信息可短暂不一致,保证服务发现可用 |
| Redis 主从 | 取决于配置 | 异步复制时可能丢失最新写 |
| 金融核心账务 | 更偏 C | 宁愿拒绝也不能错账 |
| 商品评论数 | 更偏 A | 短暂不一致可接受 |
第七步:BASE 和最终一致不是不管一致性
BASE:
| 概念 | 含义 |
|---|---|
| Basically Available | 基本可用,核心链路尽量能用 |
| Soft State | 中间状态可存在 |
| Eventually Consistent | 最终一致,经过重试补偿后达到正确结果 |
最终一致不是“随便错”。成熟最终一致方案必须有:
- 本地事务保证本服务数据正确。
- 消息或任务驱动下游变化。
- 消费端幂等。
- 失败重试。
- 长期失败进入死信或异常表。
- 定时补偿。
- 对账和告警。
flowchart TD
A["本地事务提交"] --> B["记录消息或发送事务消息"]
B --> C["消费者处理"]
C --> D{"处理成功吗"}
D -- "成功" --> E["最终一致"]
D -- "失败" --> F["重试"]
F --> G{"超过阈值吗"}
G -- "否" --> C
G -- "是" --> H["死信/异常表/告警"]
H --> I["补偿任务或人工处理"]第八步:一致性协议解决什么
Paxos、Raft、ZAB 这类协议不是给普通业务开发每天手写的,而是支撑协调系统、注册中心、元数据系统、分布式存储的底层能力。
它们解决的问题是:
多个节点在可能宕机、网络延迟、消息乱序的情况下,如何对某个值或某批日志达成一致。
简单对比:
| 协议 | 常见系统 | 学习重点 |
|---|---|---|
| Paxos | 理论基础,很多系统变体 | 提案、接受、多数派 |
| Raft | etcd、Consul、很多教学实现 | Leader、日志复制、任期、提交 |
| ZAB | ZooKeeper | Leader、zxid、原子广播、崩溃恢复 |
Raft 简化流程
flowchart TD
A["客户端写请求"] --> B["Leader 接收"]
B --> C["追加到 Leader 日志"]
C --> D["复制日志给 Follower"]
D --> E{"多数派确认"}
E -- "是" --> F["Leader 提交"]
F --> G["通知 Follower 提交"]
G --> H["返回客户端成功"]
E -- "否" --> I["等待或失败"]为什么要多数派?
- 两个网络分区不可能同时拥有多数派。
- 已提交日志不容易被另一个分区覆盖。
- Leader 切换时能从多数派中恢复已提交状态。
业务开发不一定要实现 Raft,但要理解:ZooKeeper、etcd 这类协调组件为什么通常牺牲部分可用性来保证一致性。
继续深入:Paxos、Raft、ZAB 与多数派 · etcd/raft 内部状态机、Ready 持久化、线性读、快照与成员变更 · 独立面试题。
第九步:幂等是分布式系统的地基
幂等是指同一个业务请求执行一次和执行多次,最终业务结果一致。
需要幂等的场景:
| 场景 | 为什么会重复 |
|---|---|
| 支付回调 | 支付平台可能多次通知 |
| MQ 消费 | 至少一次投递,ACK 失败会重投 |
| 接口重试 | 超时后调用方重试 |
| 补偿任务 | 定时任务反复扫描 |
| 用户重复点击 | 前端或网关重复请求 |
常见幂等方案:
| 方案 | 适合 |
|---|---|
| 唯一索引 | 订单号、支付流水、消息 ID |
| 幂等表 | 外部请求号、回调流水 |
| 状态机 | 订单只能按合法状态流转 |
| 乐观锁 | 更新时带版本号 |
| 分布式锁 | 短时间互斥,但不能替代幂等 |
| Token | 防重复提交 |
幂等表 Demo
create table idempotent_record (
id bigint primary key auto_increment,
biz_type varchar(64) not null,
biz_key varchar(128) not null,
status varchar(32) not null,
created_at datetime not null,
unique key uk_biz (biz_type, biz_key)
);Java 伪代码:
public void handlePaymentCallback(String payNo, String orderNo) {
boolean first = idempotentRepository.tryInsert("PAY_CALLBACK", payNo);
if (!first) {
return;
}
orderService.markPaid(orderNo, payNo);
}重点:不能先查再插,因为并发下两个线程可能都查不到。更稳的是插入唯一键,谁插入成功谁处理。
第十步:分布式 ID 为什么需要专门设计
单库单表可以用自增主键。分库分表、多服务、多机房后,自增 ID 会遇到:
- 多库自增冲突。
- 暴露业务规模。
- 不利于全局排序。
- 合并数据困难。
常见方案:
| 方案 | 优点 | 缺点 |
|---|---|---|
| UUID | 简单,本地生成 | 太长,无序,索引不友好 |
| 数据库号段 | 趋势递增,易落地 | 依赖数据库,号段服务要高可用 |
| Redis INCR | 简单递增 | 依赖 Redis,高可用和持久性要考虑 |
| Snowflake | 本地生成,趋势递增 | 时钟回拨、机器 ID 管理 |
| Leaf/UidGenerator | 工程化封装 | 需要部署和治理 |
Snowflake 典型结构:
符号位 | 时间戳 | 机器ID | 序列号不这样设计会怎样?
- ID 冲突导致唯一键异常。
- ID 无序导致 B+Tree 频繁分裂。
- 时钟回拨导致重复 ID。
- 机器 ID 配错导致多实例冲突。
第十一步:限流、熔断、降级分别做什么
这三个经常被混在一起。
| 能力 | 解决什么 | 例子 |
|---|---|---|
| 限流 | 请求太多,保护自己 | 每秒最多 1000 次下单 |
| 熔断 | 下游持续失败,快速失败 | 支付查询连续超时后短暂熔断 |
| 降级 | 非核心能力不可用时走兜底 | 推荐服务挂了返回默认推荐 |
flowchart TD
A["请求进入系统"] --> B{"是否超过限流阈值"}
B -- "是" --> C["拒绝或排队"]
B -- "否" --> D{"下游是否熔断"}
D -- "是" --> E["快速失败或兜底"]
D -- "否" --> F["调用下游"]
F --> G{"下游成功吗"}
G -- "成功" --> H["返回结果"]
G -- "失败" --> I["记录失败并可能触发熔断"]常见限流算法:
| 算法 | 原理 | 特点 |
|---|---|---|
| 固定窗口 | 一个窗口内限制总数 | 窗口边界可能突刺 |
| 滑动窗口 | 把窗口切小格统计 | 更平滑 |
| 漏桶 | 固定速度流出 | 平滑但可能排队 |
| 令牌桶 | 固定速度生成令牌 | 允许一定突发 |
第十二步:分布式锁能做什么,不能做什么
分布式锁解决的是多实例之间的互斥。
适合:
- 防止多个实例同时重建热点缓存。
- 防止多实例定时任务重复执行。
- 低频关键资源互斥。
不适合:
- 替代数据库事务。
- 替代幂等。
- 长时间包住复杂远程调用。
- 作为资金库存唯一正确性保障。
Redis 锁正确释放必须校验 token:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end为什么?
- 线程 A 加锁成功但执行太久,锁过期。
- 线程 B 获得同一把锁。
- A 执行完如果直接
del,会误删 B 的锁。 - 校验 token 可以避免删除别人的锁。
Redlock 是多 Redis 节点多数派加锁方案,但它不是强一致共识算法。关键业务仍然要靠数据库约束、状态机、幂等和补偿兜底。
第十三步:分布式事务怎么选
分布式事务不是一种技术,而是一类方案。
| 方案 | 一致性倾向 | 适合 | 代价 |
|---|---|---|---|
| XA/JTA/2PC | 强一致 | 短事务、资源支持 XA | 锁重、性能低、可用性差 |
| TCC | 较强一致 | 余额、库存、券预留 | 业务侵入大 |
| Saga | 最终一致 | 长流程、多步骤业务 | 需要补偿设计 |
| 本地消息表 | 最终一致 | 订单后置通知、积分、ES 同步 | 有延迟,需要投递任务 |
| MQ 事务消息 | 最终一致 | 本地事务和发消息一致 | 消费仍需幂等 |
| Seata AT | 框架协调 | SQL 兼容场景 | 全局锁、undo_log、限制较多 |
| 最大努力通知 | 最终一致 | 支付回调、通知类 | 依赖对方查询和补偿 |
选型流程:
flowchart TD
A["跨服务一致性问题"] --> B{"能否避免跨服务事务"}
B -- "能" --> C["调整建模或合并事务边界"]
B -- "不能" --> D{"是否必须立即强一致"}
D -- "是" --> E{"是否能资源预留"}
E -- "能" --> F["TCC"]
E -- "不能" --> G["XA/JTA/2PC 或 Seata XA/AT"]
D -- "否" --> H{"是否长流程"}
H -- "是" --> I["Saga"]
H -- "否" --> J["本地消息表 / 事务消息"]
J --> K["幂等、重试、补偿、对账"]
I --> K
F --> K
G --> K面试重点不是背方案名,而是能说明:
- 业务是否真的需要强一致。
- 失败后怎么重试。
- 重复执行怎么幂等。
- 一直失败怎么补偿。
- 最终状态怎么对账。
- 用户看到什么状态。
第十四步:缓存一致性怎么放进分布式主线
缓存一致性本质是事实源和副本之间的一致性。
flowchart TD
A["写请求"] --> B["更新数据库事实源"]
B --> C["事务提交"]
C --> D["删除 Redis 缓存"]
D --> E{"删除成功吗"}
E -- "成功" --> F["后续读回源重建缓存"]
E -- "失败" --> G["记录重试任务"]
G --> H["补偿删除或 CDC 修正"]常见原则:
- 数据库是事实源。
- Redis 是可重建副本。
- 普通缓存追求最终一致,不追求每一毫秒强一致。
- 删除缓存失败必须重试或补偿。
- 关键交易状态不要只信缓存。
第十五步:ES 同步一致性怎么放进分布式主线
MySQL 和 ES 也是事实源和搜索视图的关系:
flowchart TD
A["MySQL 业务数据变更"] --> B["Binlog CDC 或 MQ"]
B --> C["同步消费者"]
C --> D["写入 Elasticsearch"]
D --> E{"写入成功吗"}
E -- "成功" --> F["搜索视图更新"]
E -- "失败" --> G["重试 / 死信 / 补偿任务"]
G --> C如果 ES 更新失败怎么办?
- 不要静默丢弃。
- 记录失败事件。
- 可重试错误自动重试。
- 不可重试错误进入死信并告警。
- 定时按 MySQL 事实源补偿。
- 严重时通过别名重建索引。
第十六步:生产排查总流程
flowchart TD
A["分布式线上问题"] --> B{"表现是什么"}
B -- "接口慢" --> C["看链路追踪和各段耗时"]
B -- "错误率高" --> D["看异常、熔断、下游健康"]
B -- "数据不一致" --> E["查事实源、消息、补偿、对账"]
B -- "消息堆积" --> F["看生产TPS、消费TPS、Lag分布"]
B -- "服务找不到" --> G["查注册中心、版本、分组、本地缓存"]
C --> H["定位慢服务/慢SQL/慢下游"]
D --> I["限流、降级、熔断或回滚"]
E --> J["补偿修复并补幂等/告警"]
F --> K["扩容、限流、修慢消息、处理热点"]
G --> L["修注册、网络、配置和发布版本"]排查不能只看一个服务日志。要形成证据链:
| 证据 | 看什么 |
|---|---|
| traceId | 一次请求跨服务经过哪里 |
| 指标 | QPS、RT、错误率、线程池、连接池 |
| 日志 | 参数、业务状态、异常栈 |
| MQ | Lag、重试、死信、消费耗时 |
| 数据库 | 慢 SQL、锁等待、事务、主从延迟 |
| 缓存 | 命中率、大 key、热 key、超时 |
| 注册中心 | 实例是否存在、健康状态、版本分组 |
| 配置中心 | 开关、限流阈值、灰度规则 |
商业场景一:下单扣库存
问题
下单需要:
- 创建订单。
- 扣减或锁定库存。
- 使用优惠券。
- 发支付单。
如果这些在不同服务,任意一步失败都会导致状态不一致。
推荐设计
flowchart TD
A["创建订单<br/>本地事务"] --> B["订单状态: 待支付"]
B --> C["发送库存锁定消息"]
C --> D["库存服务幂等锁库存"]
D --> E{"库存成功吗"}
E -- "成功" --> F["订单可支付"]
E -- "失败" --> G["订单关闭或等待补偿"]
F --> H["支付成功回调"]
H --> I["幂等更新订单为已支付"]落地要点:
- 订单号唯一。
- 库存锁定有幂等号。
- 支付回调幂等。
- 订单状态机限制状态跳转。
- 消息失败有重试和死信。
- 定时任务扫描超时订单释放库存。
商业场景二:医疗数据采集平台
问题
医疗数据采集平台常见链路:
- 调度采集任务。
- 调用医院接口或读取文件。
- 清洗转换。
- 写入数据库。
- 同步搜索索引。
- 记录异常和资产变更。
分布式风险:
| 风险 | 后果 |
|---|---|
| 任务重复调度 | 重复采集、重复入库 |
| 接口超时 | 不知道医院系统是否处理 |
| 消息堆积 | 数据同步延迟 |
| ES 写入失败 | 搜索结果不更新 |
| Redis 缓存旧数据 | 页面显示旧资产状态 |
推荐设计
- 采集任务有
taskId + sourceId + batchNo幂等键。 - 原始数据先落库或落对象存储,避免处理中断丢数据。
- 清洗入库使用唯一约束防重复。
- 资产变更通过 MQ 同步 ES。
- ES 失败进入重试和死信。
- Redis 缓存由数据库变更后删除,失败补偿。
- XXL-JOB 分片执行任务,任务本身仍要幂等。
- 全链路 traceId 贯穿调度、采集、入库、同步。
常见误区
| 误区 | 为什么错 | 正确理解 |
|---|---|---|
| 微服务一定比单体高级 | 分布式复杂度很高 | 业务规模和团队需要时再拆 |
| 注册中心转发请求 | 注册中心只保存地址 | Consumer 直连 Provider |
| 超时就是失败 | 下游可能已执行成功 | 要查询确认和幂等 |
| 重试能解决所有问题 | 重试会放大流量和重复执行 | 必须配合幂等和退避 |
| 分布式事务就是 Seata | Seata 只是框架 | 还有 XA、TCC、Saga、消息 |
| 最终一致就是不保证一致 | 最终一致要重试、补偿、对账 | 只是接受短暂中间状态 |
| 分布式锁能保证正确性 | 锁会过期、误删、脑裂 | 还要数据库约束和状态机 |
| CAP 三选二 | P 是必须面对的现实 | 分区时在 C 和 A 间取舍 |
面试标准回答
分布式系统怎么从零学到生产级
我会先理解分布式系统的本质:多个服务通过网络协作,网络可能失败、超时和重复,状态分散在不同服务和数据库中。然后按远程调用、注册发现、负载均衡、限流熔断、CAP/BASE、一致性设计、幂等、分布式事务、分布式锁、缓存一致性、消息可靠性和可观测性这条线学习。生产落地时重点不是用了哪个框架,而是超时后如何确认状态,重复请求如何幂等,失败如何补偿,数据如何对账,线上如何通过 traceId、指标、日志和消息状态定位问题。CAP 怎么理解
CAP 不是简单三选二。分布式系统必须考虑网络分区 P;当分区发生时,系统只能在一致性 C 和可用性 A 之间取舍。比如 ZooKeeper 更偏 CP,少数派不可写以保证一致性;Eureka 更偏 AP,允许注册信息短暂不一致以保证服务发现可用。业务选型要看数据风险,资金账务更偏一致性,评论数、浏览量可以接受最终一致。分布式事务怎么选
分布式事务要先看业务是否能避免跨服务事务,能合并边界就不要拆。必须跨服务时,如果需要强一致且事务短,可以考虑 XA/JTA/2PC 或 Seata XA/AT;如果业务能做资源预留,适合 TCC;长流程适合 Saga;订单后置通知、积分、ES 同步等更适合本地消息表或事务消息。无论哪种方案,都必须配套幂等、重试、补偿、对账和告警。为什么分布式系统必须做幂等
因为远程调用、MQ 消费、支付回调、补偿任务都可能重复执行。超时后调用方不知道下游是否成功,通常会重试;MQ 至少一次投递也可能重复消费。如果没有幂等,就可能重复扣款、重复发券、重复写入。常用方案有唯一索引、幂等表、状态机、乐观锁和业务流水号。关联知识点
| 知识点 | 继续学习 |
|---|---|
| 从零到精通验收清单 | 分布式系统从零到精通验收清单 |
| 商业场景训练营 | 分布式系统商业场景训练营 |
| 分布式系统基础 | 基础架构 |
| CAP、BASE 与一致性理论 | CAP与BASE |
| 数据一致性设计 | 数据一致性 |
| 幂等设计 | 幂等设计 |
| 分布式事务总览 | 分布式事务 |
| XA/JTA/2PC | XA/JTA/2PC |
| TCC | TCC |
| Saga | Saga |
| 可靠消息与本地消息表 | 可靠消息 |
| Seata | Seata |
| 分布式事务选型 | 选型与排查 |
| 共识算法主线 | Paxos、Raft、ZAB与多数派 |
| Raft内部原理 | etcd/raft状态机、线性读与故障恢复 |
| 共识与Raft面试 | 独立面试题 |
| ZooKeeper | ZooKeeper |
| Dubbo | Dubbo |
| Service Mesh 基础主线 | Envoy、xDS、mTLS 与流量治理 |
| Service Mesh 内部原理 | Envoy 运行时、xDS 收敛与生产治理 |
| Redisson 分布式锁 | Redisson 锁 |
| Redis 红锁 | Redlock |
| 缓存一致性 | Redis 与数据库缓存一致性 |
| MQ 堆积 | 消息堆积与背压 |
| ES 一致性 | MySQL 与 ES 一致性 |
本章小结
分布式系统不是靠某个框架变可靠,而是靠一组工程原则变可靠:超时、重试、幂等、状态机、注册发现、限流、熔断、降级、锁、事务、消息、补偿、对账、监控和追踪。学习时不要从框架名开始,而要从“网络不可靠、状态会分散、一致性有代价”开始。只要这条主线建立起来,Spring Cloud、Dubbo、ZooKeeper、RocketMQ、Redis 锁、Seata、ES 同步这些技术就能串成一个完整体系。
