已完成分布式系统本质与挑战

约 25 分钟 · 阶段:分布式基石 · 能力:解释部分失败并画出 ShopFlow 三上下文

为什么要学本章

这是全课起点。若把远程调用当本地方法、把超时当「偶发 bug」,后面的 CAP、Saga、熔断都会变成背公式。本章用 ShopFlow 下单 把分布式「默认会出事」讲清楚。

学完你能
  • 用业务语言说明为何要从单机走向分布式(至少 2 条驱动力)
  • 口述 POST /reserve 超时后「调用方不确定」的两种真实状态及应对思路
  • 为秒杀 / 对账场景排序扩展 · 可用 · 一致,并衔接 CAP、BASE 与一致性模型
本章先记住 3 件事
  • 为何分布式:多节点协作换扩展与高可用,代价是部分失败与一致性更难。
  • 设计先问什么:扩展性、可用性、一致性之间显式取舍(详见 CAP、BASE 与一致性模型 中的 CAP)。
  • 本课主线:Java + Spring Cloud 落地 ShopFlow,原理 → 实战 → 生产治理。

单机到集群:为何需要分布式架构

当用户量、数据量或发布频率超过单机/单库舒适区,团队会把系统拆到多台机器上协作。常见驱动力:

  • 水平扩展 — 加机器分担读/写吞吐,而不是无限堆单机配置。
  • 故障隔离 — 单点故障只影响局部;配合副本与自动切换提高可用性。
  • 组织与交付 — 多团队并行演进不同业务能力(与 康威定律 相关,架构风格:单体、SOA 与微服务 展开)。
  • 地理与合规 — 多机房、多区域部署降低延迟或满足数据驻留要求。

何时值得认真考虑拆分? 不必等到「千万 QPS」才行动,下列信号出现时应把分布式纳入架构讨论(满足其一即可启动评估):

  • 单机或单库连接池、CPU、磁盘 IO 长期打满,垂直扩容性价比明显下降。
  • 发布耦合:改一个小模块需要全量回归、全量部署,交付周期被拖慢。
  • 可用性目标 要求多 AZ/多机房与明确 RTO/RPO,单机故障域无法接受。
  • 组织规模 扩大,多团队需并行演进不同业务线。
单体应用

开发简单、事务本地;扩展时往往整包复制,数据库易成瓶颈。

分布式系统

能力可独立扩展;需要处理网络调用、一致性与运维复杂度。

架构设计职责

划定边界、选通信方式、定义失败语义与可观测性,而不是「微服务越多越好」。

远程调用:反例 vs 推荐做法

不推荐
  • 无超时,线程一直阻塞
  • 把 Feign 当本地方法,异常吞掉
  • 超时后无幂等,立即重试 N 次
  • 用墙钟时间排序跨服务事件
推荐
先想 10 秒:没有分布式时,大促峰值你会先动哪一招?

常见:垂直扩容、读写分离、缓存。分布式是多机 + 明确失败语义,不是单机优化的换皮。

网络分区、时钟漂移与部分失败

分布式系统的默认假设是:网络不可靠、节点会挂、时钟不完全同步。这三点直接决定你不能把远程调用当成本地方法调用。

  • 网络分区 — 节点之间暂时不可达,但各自仍在运行;此时「全局一致决策」可能无法同时满足。
  • 时钟漂移 — 即使部署 NTP,各节点墙钟仍可能偏差;不要用 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,而不是无限重试。

调用方视角 × 服务端真实状态

调用方看到服务端可能真实状态典型应对
超时 / 连接失败 已成功(响应丢失) 幂等键 + 查询预留状态;避免盲目重试导致重复扣减
超时 / 连接失败 未执行 带幂等键的安全重试;记录业务请求号
收到成功响应 实际失败(少见) 对账任务、状态机补偿、告警
重复提交同一业务键 已处理过 幂等返回原结果

超时后重试:代码层反例与推荐

不推荐 · 盲目重试
java
// 无幂等键、无上限 — 可能重复扣库存
for (int i = 0; i < 10; i++) {
  try {
    inventoryClient.reserve(orderId, skuId, qty);
    break;
  } catch (Exception e) {
    Thread.sleep(100);
  }
}
推荐 · 幂等 + 有限次
java
// 同一 requestId:服务端已处理则直接返回原结果
ReserveResult r = inventoryClient.reserve(
  new ReserveCommand(requestId, orderId, skuId, qty));
if (r.isUnknown()) {
  r = inventoryClient.getReserveStatus(requestId);
}
// 仍不确定 → 标记订单「待对账」,勿直接成功

读超时设多少?(示意滑块)

2000 ms

可扩展性、可用性与一致性三角

工程上常同时追求三者,但在故障与负载压力下必须排序优先级。例如:

目标典型手段代价/注意
可扩展性 无状态服务、分片、缓存、异步削峰 数据局部性、跨分片查询变难
可用性 多副本、健康检查、熔断、多活/主备 副本滞后时读到旧数据
一致性(详见 CAP、BASE 与一致性模型:强一致 / 最终一致) 强一致事务、共识协议、最终一致 + 对账 延迟上升、可用性在分区时受限

场景决策:大促秒杀 vs 财务对账

场景优先(1 最重要)可让步(3)一句话理由
大促秒杀 可扩展性 强一致(接受短暂不一致 + 对账) 峰值吞吐优先,库存可异步校准
财务对账报表 一致性 可扩展性(批处理窗口内完成即可) 金额差错成本高,宁可慢一点也要可对账

CAP、BASE 与一致性模型 将用 CAP 定理 与 BASE 把上表落到可讨论的取舍语言。

常见误区
  • 微服务越多越好 — 边界不清时,分布式复杂度会压过拆分收益。
  • 远程调用 = 本地方法 — 必须显式定义超时、失败语义与可观测性。
  • 超时后无限重试 — 可能打爆下游;需退避、熔断与幂等。
  • 同步 NTP 就够了 — 不能依赖墙钟做跨节点因果排序。

Java 技术栈在分布式中的常见角色

本课实践以 JVM 企业栈为主。常见分工如下:

  • 应用运行时 — Spring Boot、Actuator 健康检查。
  • 服务治理 — 注册发现、API 网关、配置中心。
  • 服务间通信 — OpenFeign(同步)、Spring Cloud Stream(异步)。
  • 数据与一致性 — 本地事务;跨服务用 Saga、Outbox、Seata。

通信模式:同步 vs 异步(标签页)

适用:需要立即知道成败的链路,如「下单当场预留库存」。

代价:延迟叠加、级联故障、部分失败(本章重点)。

ShopFlow:order → inventory 的 POST /reserve(详见 OpenFeign、负载均衡与服务间调用)。

适用:解耦、削峰、最终一致,如「支付完成通知订单」。

代价:复杂度在消费者、幂等、顺序与死信队列。

ShopFlow:PaymentCompleted 事件(详见 Kafka 事件驱动与 Redis 缓存)。

ShopFlow 逻辑分层(预习)

客户端 → 网关 → 领域服务
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 缓存。统一入口做鉴权与路由。

服务间调用:超时须显式配置(预览)

yaml
spring:
  cloud:
    openfeign:
      client:
        config:
          inventory-service:
            connectTimeout: 2000
            readTimeout: 2000

本课 ShopFlow 示例业务全景

ShopFlow 按 限界上下文 划分三大域:

上下文核心职责典型事件/接口
订单 Order 创建订单、状态机、对外查询 POST /orders、OrderCreated
库存 Inventory 预留/释放库存、防超卖 POST /reserve、幂等扣减
支付 Payment 支付单、回调、失败补偿 PaymentCompleted、Saga 补偿

常见问题(FAQ)

有了 Kafka,是不是就不用 OpenFeign 了?
不是二选一。需要当场知道成败的链路(预留库存)仍常用同步 HTTP;可延迟、可重放的通知(支付完成)更适合事件。ShopFlow 两种都会用到。
微服务是不是拆得越细越好?
否。边界不清会带来分布式事务、排障与发布协调成本。架构风格:单体、SOA 与微服务 会讲「何时不应拆」。
超时设很长,是不是就不会部分失败了?
只会把「失败发现」推迟,并占用连接/线程。网络分区、进程崩溃仍会发生;仍需幂等、查询与对账。

本章结论

放在正文与复习之间,便于你先串起全章再做题。每条对应上文一个小节标题。

  • 为何走向分布式:用多机协作换扩展与故障隔离;常见动机是容量、交付节奏或可用性,而不是「为了微服务而微服务」。
  • 部分失败是常态:网络、节点、时钟都不可靠;调用超时后先幂等与查状态,避免无界重试把故障放大。
  • 三角要显式排序:扩展、可用、一致无法同时拉满;秒杀、对账、详情页等场景的优先级不同,CAP、BASE 与一致性模型 将命名这些取舍。
  • Java 栈分工:Boot 承载服务与可观测;Cloud 负责治理与同步/异步通信;跨服务写路径用 Saga、Outbox 等模式兜一致。
  • ShopFlow 主线:Order / Inventory / Payment 三个限界上下文;同步 POST /reserve 预留库存,PaymentCompleted 事件异步收尾支付。

复习与自检

过关清单(勾选会保存在本浏览器)

0 / 5 已勾选

本章速查卡
概念记住一句
部分失败调用方无法确定远端是否已成功
幂等键同一业务请求重复提交不产生副作用
三角取舍没有免费午餐,场景决定 1/2/3 排序
默认假设网络不可靠、节点会挂、时钟不同步
答辩 / 面试口述题
  1. 为什么说「分布式系统里,超时是一种正常结果」?
  2. 如果 inventory 已成功、order 认为失败,用户界面该显示什么?架构上如何兜底?
  3. 同步调用链路过长会有什么问题?如何用异步缓解?

下一步:勾选过关清单、完成右侧测验与下方 Demo;下一章 CAP、BASE 与一致性模型 在 CAP/BASE 框架下细化本章的三角取舍。

动手练习

用本章 ShopFlow 场景,完成驱动力对照、部分失败四格表与三上下文职责三份纸面产出(约 20 分钟)。

操作步骤

  1. 对照正文「单体 / 分布式 / 架构职责」role-cards,为 ShopFlow 大促写出至少 2 条「为何需要分布式」驱动力(如水平扩展、故障隔离、多团队并行)。
  2. 填写「调用方视角 × 服务端真实状态」表中超时一行三列;并对照 Mermaid「部分失败:调用方视角不确定」口述两种真实状态及应对(查询/对账/幂等)。
  3. 对照正文三上下文表,用一句话说明 Inventory 拥有 POST /reserve 权、Order 不能直接改库存列的原因(限界上下文数据所有权)。
  4. (可选)完成 demos/basics-01-distributed-fundamentals-lab/ 练习 1~2,与上述步骤交叉核对。

判断练习

  1. 排障顺序:同事给出 ① 全体重启 ② readTimeout 调到 30s ③ 失败重试 10 次 ④ 仍失败再对账。请指出最应先做与最危险的一步。

    参考答案

    最应先做:④ 对账/查询,先消除「是否已成功」的不确定。最危险:③ 盲目重试 10 次,可能在 inventory 已扣减时重复提交,放大部分失败。加长超时 ② 仅掩盖问题。

  2. 找错:哪条违反「部分失败」假设?① 超时=inventory 一定没扣减 ② 幂等键+查询状态 ③ 用本机时间排序跨节点事件

    参考答案

    答:① 和 ③。① 超时后远端可能已成功;③ 时钟漂移下墙钟不能作全局序。② 是正确应对。

Demo:ShopFlow 上下文与失败场景实验室

目录:demos/basics-01-distributed-fundamentals-lab/。验收:能独立画完「order 调 inventory 超时」时序,并填好四格表中「超时」行。

延伸学习