开发简单、事务本地;扩展时往往整包复制,数据库易成瓶颈。
这是全课起点。若把远程调用当本地方法、把超时当「偶发 bug」,后面的 CAP、Saga、熔断都会变成背公式。本章用 ShopFlow 下单 把分布式「默认会出事」讲清楚。
POST /reserve 超时后「调用方不确定」的两种真实状态及应对思路当用户量、数据量或发布频率超过单机/单库舒适区,团队会把系统拆到多台机器上协作。常见驱动力:
何时值得认真考虑拆分? 不必等到「千万 QPS」才行动,下列信号出现时应把分布式纳入架构讨论(满足其一即可启动评估):
开发简单、事务本地;扩展时往往整包复制,数据库易成瓶颈。
能力可独立扩展;需要处理网络调用、一致性与运维复杂度。
划定边界、选通信方式、定义失败语义与可观测性,而不是「微服务越多越好」。
常见:垂直扩容、读写分离、缓存。分布式是多机 + 明确失败语义,不是单机优化的换皮。
分布式系统的默认假设是:网络不可靠、节点会挂、时钟不完全同步。这三点直接决定你不能把远程调用当成本地方法调用。
System.currentTimeMillis() 在跨服务间排序事件。sequenceDiagram participant U as 用户 participant O as order-service participant I as inventory-service U->>O: 提交订单 O->>I: POST /reserve (timeout=2s) Note over I: 库存已扣减 I--xO: 响应在网络中丢失 Note over O: 重试?幂等?人工对账?
架构上要为「不确定结果」准备幂等键、查询补偿或 Saga,而不是无限重试。
| 调用方看到 | 服务端可能真实状态 | 典型应对 |
|---|---|---|
| 超时 / 连接失败 | 已成功(响应丢失) | 幂等键 + 查询预留状态;避免盲目重试导致重复扣减 |
| 超时 / 连接失败 | 未执行 | 带幂等键的安全重试;记录业务请求号 |
| 收到成功响应 | 实际失败(少见) | 对账任务、状态机补偿、告警 |
| 重复提交同一业务键 | 已处理过 | 幂等返回原结果 |
// 无幂等键、无上限 — 可能重复扣库存
for (int i = 0; i < 10; i++) {
try {
inventoryClient.reserve(orderId, skuId, qty);
break;
} catch (Exception e) {
Thread.sleep(100);
}
}
// 同一 requestId:服务端已处理则直接返回原结果
ReserveResult r = inventoryClient.reserve(
new ReserveCommand(requestId, orderId, skuId, qty));
if (r.isUnknown()) {
r = inventoryClient.getReserveStatus(requestId);
}
// 仍不确定 → 标记订单「待对账」,勿直接成功
工程上常同时追求三者,但在故障与负载压力下必须排序优先级。例如:
| 目标 | 典型手段 | 代价/注意 |
|---|---|---|
| 可扩展性 | 无状态服务、分片、缓存、异步削峰 | 数据局部性、跨分片查询变难 |
| 可用性 | 多副本、健康检查、熔断、多活/主备 | 副本滞后时读到旧数据 |
| 一致性(详见 CAP、BASE 与一致性模型:强一致 / 最终一致) | 强一致事务、共识协议、最终一致 + 对账 | 延迟上升、可用性在分区时受限 |
| 场景 | 优先(1 最重要) | 可让步(3) | 一句话理由 |
|---|---|---|---|
| 大促秒杀 | 可扩展性 | 强一致(接受短暂不一致 + 对账) | 峰值吞吐优先,库存可异步校准 |
| 财务对账报表 | 一致性 | 可扩展性(批处理窗口内完成即可) | 金额差错成本高,宁可慢一点也要可对账 |
CAP、BASE 与一致性模型 将用 CAP 定理 与 BASE 把上表落到可讨论的取舍语言。
本课实践以 JVM 企业栈为主。常见分工如下:
适用:需要立即知道成败的链路,如「下单当场预留库存」。
代价:延迟叠加、级联故障、部分失败(本章重点)。
ShopFlow:order → inventory 的 POST /reserve(详见 OpenFeign、负载均衡与服务间调用)。
适用:解耦、削峰、最终一致,如「支付完成通知订单」。
代价:复杂度在消费者、幂等、顺序与死信队列。
ShopFlow:PaymentCompleted 事件(详见 Kafka 事件驱动与 Redis 缓存)。
flowchart LR Client[Web / App] GW[API Gateway] Order[order-service] Inv[inventory-service] Pay[payment-service] Client --> GW GW --> Order Order -->|同步 HTTP| Inv Order -.->|异步事件| Pay
实线:OpenFeign、负载均衡与服务间调用;虚线:Kafka 事件驱动与 Redis 缓存。统一入口做鉴权与路由。
spring:
cloud:
openfeign:
client:
config:
inventory-service:
connectTimeout: 2000
readTimeout: 2000
ShopFlow 按 限界上下文 划分三大域:
| 上下文 | 核心职责 | 典型事件/接口 |
|---|---|---|
| 订单 Order | 创建订单、状态机、对外查询 | POST /orders、OrderCreated |
| 库存 Inventory | 预留/释放库存、防超卖 | POST /reserve、幂等扣减 |
| 支付 Payment | 支付单、回调、失败补偿 | PaymentCompleted、Saga 补偿 |
放在正文与复习之间,便于你先串起全章再做题。每条对应上文一个小节标题。
POST /reserve 预留库存,PaymentCompleted 事件异步收尾支付。过关清单(勾选会保存在本浏览器)
0 / 5 已勾选
| 概念 | 记住一句 |
|---|---|
| 部分失败 | 调用方无法确定远端是否已成功 |
| 幂等键 | 同一业务请求重复提交不产生副作用 |
| 三角取舍 | 没有免费午餐,场景决定 1/2/3 排序 |
| 默认假设 | 网络不可靠、节点会挂、时钟不同步 |
下一步:勾选过关清单、完成右侧测验与下方 Demo;下一章 CAP、BASE 与一致性模型 在 CAP/BASE 框架下细化本章的三角取舍。
用本章 ShopFlow 场景,完成驱动力对照、部分失败四格表与三上下文职责三份纸面产出(约 20 分钟)。
POST /reserve 权、Order 不能直接改库存列的原因(限界上下文数据所有权)。demos/basics-01-distributed-fundamentals-lab/ 练习 1~2,与上述步骤交叉核对。排障顺序:同事给出 ① 全体重启 ② readTimeout 调到 30s ③ 失败重试 10 次 ④ 仍失败再对账。请指出最应先做与最危险的一步。
最应先做:④ 对账/查询,先消除「是否已成功」的不确定。最危险:③ 盲目重试 10 次,可能在 inventory 已扣减时重复提交,放大部分失败。加长超时 ② 仅掩盖问题。
找错:哪条违反「部分失败」假设?① 超时=inventory 一定没扣减 ② 幂等键+查询状态 ③ 用本机时间排序跨节点事件
答:① 和 ③。① 超时后远端可能已成功;③ 时钟漂移下墙钟不能作全局序。② 是正确应对。
目录:demos/basics-01-distributed-fundamentals-lab/。验收:能独立画完「order 调 inventory 超时」时序,并填好四格表中「超时」行。