← 全部课程

Java 分布式架构设计入门到进阶

0/0 (0%)

Java 分布式架构设计

从 CAP、领域建模与通信模式,到 Spring Cloud 微服务实战(ShopFlow 示例),再到韧性、可观测与架构演进——按三阶段系统掌握 Java 分布式架构设计与落地。

当前版本 Java 21 LTS;Spring Boot 3.5.x;Spring Cloud 2025.0.x
大纲规模 15 章 · 3 阶段 · 已生成 1 章正文
前置要求 Java 17+ · Spring Boot · HTTP/REST · Maven
官方文档 Spring Cloud Reference
领域类型 F — 横切能力(架构设计 + Java 微服务栈)
学习路径 原理与建模 → ShopFlow 实战 → 生产治理与演进
版本说明(2026-05-18): 实践章以 Spring Boot 3.5 与 Spring Cloud 2025 发布列车为准;对接企业内旧版集群时请以实际 BOM 与官方发行说明为准。

三阶段大纲摘要

阶段章节小节概要

阶段一 · 分布式基石

CAP/BASE、架构风格选型、DDD 边界与 API 契约、同步/异步通信模式;为 ShopFlow 拆分打底。

阶段二 · ShopFlow 微服务实战

Spring Cloud 脚手架、注册发现与网关、OpenFeign 调用、Kafka/Redis、分布式事务与 Saga mini 项目。

阶段三 · 生产治理与演进

熔断限流、可观测性、安全与 K8s 部署、容量扩展、CQRS/反模式与架构评审。

请从左侧目录选择章节;建议从「分布式系统本质与挑战」读起。打开过的章节侧栏显示圆点(进行中),测验与过关清单完成后可点章首标记完成(✓)。进度与清单勾选可通过顶栏导出/导入。

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

约 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 超时」时序,并填好四格表中「超时」行。

延伸学习

已完成CAP、BASE 与一致性模型

约 30 分钟 · 阶段:分布式基石 · 能力:按接口说明 CAP 取舍与一致级别

为什么要学本章

分布式系统本质与挑战 讲了「部分失败」;本章回答:分区发生时在 C 与 A 之间选什么、为何常见 AP + 最终一致,以及「读己之写」从哪来。

学完你能
  • 为 ShopFlow 的 POST /reserve、商品详情、支付回调各说一句分区时的 CP/AP 取向
  • 区分强一致、最终一致、因果一致,并解释「最终一致 ≠ 随便脏读」
  • 说明 fencing token 防「旧主覆写」
本章先记住 3 件事
  • CAP:发生网络分区时,只能在一致性 C 与可用性 A 之间取舍;分区容忍 P 在分布式里通常不能放弃。
  • 工程现实:多数互联网业务选 AP + 最终一致(BASE),用对账、幂等与 Outbox 兜底。
  • 客户端语义:除存储层一致级别外,还要定义「用户读到自己刚写的数据」等会话级保证。

CAP 定理与工程中的真实取舍

CAP 定理(Brewer)指出:在存在网络分区时,分布式系统无法同时满足:

  • C(Consistency) — 每次读都能读到最新写入,或返回错误(线性一致视角的简化理解)。
  • A(Availability) — 每个请求都能得到非错误响应(不要求读到最新)。
  • P(Partition tolerance) — 分区发生后系统仍能继续工作(节点间消息可能丢失/延迟)。

真实部署几乎必须接受 P,于是分区发生时要在 C 与 A 间做选择——不是「三选二」的日常开关,而是按业务路径分别设计。

分区时的二选一(概念图)

发生分区 · CP 与 AP 路径
flowchart TB
  subgraph normal [无分区]
    W[写入成功] --> R[读到最新]
  end
  subgraph partition [发生分区 P]
    CP[选 CP] -->|拒绝部分读写| ERR[返回错误 / 降级]
    AP[选 AP] -->|继续服务| STALE[可能读到旧副本]
  end
  normal --> partition

CP:宁可失败也不返回不确定的旧数据;AP:宁可旧数据也要保持入口可用。

对照:CP 与 AP 在 ShopFlow 中的例子

偏 CP 的路径
  • 支付扣款:余额不足必须立即拒绝,不能「先成功后对账」
  • 库存强一致扣减(少数场景):超卖代价极高时用共识/锁
偏 AP 的路径
  • 商品详情页:可短暂展示旧库存,后台异步校准
  • 订单列表:最终一致 即可,配合状态机与对账

PACELC 延伸(了解即可)

无分区时仍要在延迟(L)与一致(C)间权衡:例如跨地域复制常降低一致级别以换更低延迟。设计评审时可问:「正常时要多一致?分区时要多可用?」

常见误区
  • 给整个系统贴一个 CP/AP 标签 — 应按接口/数据分别决策。
  • 认为选 AP 就不要一致性 — 最终一致 + 对账同样是一致性策略。
  • 忽略 P — 局域网也会分区,云多 AZ 更常见。
先想 10 秒:大促分区时,商品详情页更应该保 C 还是 A?

多数选 A(短暂旧库存 + 异步校准);支付/防超卖路径才靠近 C。勿对全站贴单一 CAP 标签。

强一致、最终一致与因果一致

「一致性」在不同语境含义不同:数据库事务、副本同步、客户端会话。下表是架构讨论中最常用的划分。

模型含义(简化)典型实现/场景
强一致 / 线性一致 所有节点如同一份拷贝,读写全局有序 etcd/ZooKeeper、部分分布式锁;代价是延迟与可用性
最终一致 停止写入后,副本会在有限时间内收敛 Cassandra、CouchDB、Kafka 消费滞后;须幂等与对账
因果一致 有因果关系的操作被所有节点按因果序看到 会话粘滞 + 版本向量;评论回复链

序列图:最终一致下的读延迟

写主库 · 读从库可能短暂旧值
sequenceDiagram
  participant App as order-service
  participant M as DB 主
  participant R as DB 从
  App->>M: UPDATE stock=9
  M-->>App: OK
  App->>R: SELECT stock
  R-->>App: stock=10 (复制滞后)
  Note over M,R: 异步复制追上后读到 9

practice 阶段读写分离时会再遇此问题;缓解:读主、版本号、或接受短暂旧读 + 业务校验。

场景决策:接口选哪种一致级别

ShopFlow 接口建议理由
POST /reserve 强一致或串行化单点 防超卖;可接受短暂不可用
GET /products/{id} 最终一致 + 缓存 读多写少,短暂旧库存可容忍
支付回调更新订单 幂等 + 至少一次投递 跨服务;靠业务键去重(Saga 章节展开)

互动:副本读一致性级别(示意)

QUORUM(2/3)

BASE 与互联网业务常见策略

BASE 是对 AP 路径的实操总结(与 ACID 相对):

  • Basically Available — 核心路径尽量有响应,非核心可降级。
  • Soft state — 中间状态允许存在(处理中、待对账)。
  • Eventually consistent — 通过复制、消息、定时任务达到一致。

ACID vs BASE(单服务 vs 跨服务)

维度ACID(单库事务)BASE(跨服务协作)
边界 一个数据库连接内 多服务、多库、消息
失败 回滚 补偿、重试、对账
ShopFlow 单服务内订单行 + 明细 下单 → 库存 → 支付链(详见 分布式数据:事务、幂等与 Saga)

代码:Transactional Outbox 预览

「写订单表 + 发 Kafka」无法靠本地事务一起提交。常见做法:Outbox 与业务写入同一事务,再由投递器异步发消息。

java
@Transactional
public void createOrder(CreateOrder cmd) {
  orderRepository.save(cmd.toOrder());
  outboxRepository.save(OutboxEvent.orderCreated(cmd.orderId()));
  // 同一 DB 事务;消息由 poller 异步投递,消费者须幂等
}

互联网常见组合拳

  • 幂等键 — 重复请求返回同一业务结果。
  • 对账任务 — 日终比对订单、支付、库存流水。
  • 状态机 — 订单 CREATED → PAID,非法跳转拒绝。
  • 降级开关 — 分区时关闭非核心功能,保护核心下单路径。

分布式锁、租约与 fencing token 概念

跨节点互斥(定时任务抢主、库存扣减串行化)常用分布式锁。实现须考虑:锁持有者崩溃、时钟漂移、旧锁未释放就获得新锁。

  • 租约(Lease) — 锁带 TTL;持有者须周期性续租,否则自动释放。
  • Fencing token — 单调递增令牌;下游存储拒绝比当前令牌更旧的写,防止「僵尸持有者」覆写新主。

流程:为何需要 fencing

旧主恢复后若无 fencing 可能覆写新主
sequenceDiagram
  participant N1 as 节点1(旧主)
  participant L as 锁服务
  participant DB as 存储
  N1->>L: 曾持有 lock#5
  Note over N1: GC 停顿后「以为」仍持有锁
  participant N2 as 节点2(新主)
  N2->>L: 获得 lock#6
  N2->>DB: write(token=6)
  N1->>DB: write(token=5) 
  Note over DB: 无 fencing 时旧写可能破坏新写

存储侧记录「已见过的最大 token」,拒绝更小 token 的写。

代码对比:锁值是否单调

不推荐 · 仅 UUID 锁值
java
// 下游无法区分「旧主迟到的写」
String lockVal = UUID.randomUUID().toString();
redis.set("lock:job", lockVal, NX, PX, 30000);
推荐 · 租约 + fencing token
java
long token = lockService.acquire("inventory-shard-1", Duration.ofSeconds(30));
inventoryRepository.decrement(skuId, qty, token);
// 仓储层: if (token < lastAppliedToken) reject;

Java 生态常见组件:Redis Redisson、ZooKeeper Curator、数据库 SELECT … FOR UPDATE(有单点边界)。能不用锁就不用:优先幂等、分区键串行、CRDT 等业务手段。

读己之写、单调读等客户端语义

即使存储标记为「最终一致」,产品仍常要求会话级体验保证(由网关粘滞、读写路由或版本号实现):

语义用户可感知的效果ShopFlow 例子
读己之写(Read-your-writes) 刚下单后刷新列表能看到自己的订单 写主库后读主或带 userId 路由到含最新会话的副本
单调读(Monotonic reads) 不会先看到新评论再看到旧评论 订单时间线只前进;用递增 version 过滤
单调写(Monotonic writes) 同一用户的写按顺序可见 同一 orderId 状态变更串行处理
写跟随(Writes-follow-reads) 改头像后评论里头像一起更新 用户资料变更广播或短期读主

配置预览:读写分离时的「读主」开关

yaml
shopflow:
  datasource:
    read-your-writes: true
    # 创建订单后的 5s 内,该 userId 查询走主库

FAQ

最终一致 = 可以随便脏读?
否。须定义收敛时间、业务补偿与客户端语义;金融类路径仍可能要求强一致。
Redis 分布式锁够吗?
简单场景可用;关键写路径建议租约 + fencing 或改用共识组件;注意主从切换时的锁安全性(Redlock 争议见延伸阅读)。

下一章讨论 架构风格:单体、SOA 与微服务,把本章取舍落到架构形态选择。

本章结论

本章回答「分区与复制时数据有多新」。下面几条是验收口径,可对照各节与 ShopFlow 接口回忆。

  • CAP 的工程问法:真实部署须接受分区容忍 P;分区发生时只能在一致性 C 与可用性 A 间取舍,且应按接口/数据集分别决策,勿给全系统贴单一 CP/AP 标签。
  • 一致模型落到接口:强一致、最终一致、因果一致含义不同;POST /reserve 防超卖宜强一致或串行化,商品详情等读多场景可接受短暂旧读。
  • BASE 是 AP 路径的实操:基本可用 + 软状态 + 最终一致;跨服务靠幂等、对账、状态机与 Outbox 等组合拳收敛。
  • 锁是最后手段:优先幂等与业务串行;必须用分布式锁时要租约续期与 fencing token,防止旧主迟到的写破坏新主。
  • 客户端语义不可省:除存储层一致级别外,还须定义读己之写等会话体验(粘滞、短期读主、版本号)。

复习与自检

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

0 / 5 已勾选

本章速查卡
概念一句
CAP有 P 时分区要在 C/A 间选
最终一致停止写后会收敛,运行中可能旧读
BASE可用 + 软状态 + 最终一致 + 补偿
Fencing单调 token,存储拒绝旧写
读己之写用户看到自己刚提交的数据
答辩 / 面试口述题
  1. 为什么说「整个公司是 CP 系统」这句话不严谨?
  2. 下单后立刻查订单列表为空,可能是哪些一致性问题?如何修?

下一步:勾选过关清单、完成测验与 Demo;下一章 架构风格:单体、SOA 与微服务。

动手练习

为 ShopFlow 三个接口标注 CAP 取向,并完成fencing 时序与读己之写两处纸面标注(约 25 分钟)。

操作步骤

  1. 为 POST /reserve、GET /products/{id}、支付回调各写一句:分区时偏 CP 还是 AP,并引用正文 CAP 流程图一句理由。
  2. 对照 Mermaid「旧主恢复后若无 fencing 可能覆写新主」,补写:有 fencing 时,旧 token 的写请求会被存储层拒绝。
  3. 填写「读己之写 / 单调读」表:用户下单后刷新列表为空,属于哪类语义失效?写出 1 条缓解手段(会话粘滞 / 短期读主 / 版本号)。

判断练习

  1. 大促商品详情可略旧库存,但 POST /reserve 须防超卖。为何前者可偏 AP、后者偏 CP?各写一句。

    参考答案

    商品详情读多写少、短暂旧值可接受,可用 AP + 最终一致。预留库存须当场防超卖,分区时更倾向 CP/串行化或强一致路径,否则会出现双卖。

  2. 库存扣减用 Redis 锁,锁值为固定 UUID、无 fencing token。主要违反哪一节?说明「僵尸节点」风险。

    参考答案

    违反「分布式锁、租约与 fencing token」一节。租约过期后旧主仍可能持锁并写库;无递增 fencing token 时,存储无法拒绝更旧的写,新主数据可能被覆盖。

Demo:CAP 取舍与一致级别实验室

目录:demos/basics-02-consistency-cap-lab/。验收:接口卡片 3 张均已标注 CP/AP + 一句理由。

已完成架构风格:单体、SOA 与微服务

约 28 分钟 · 阶段:分布式基石 · 能力:在单体/模块化单体/SOA/微服务间做有依据的形态选择

为什么要学本章

分布式系统本质与挑战 与 CAP、BASE 与一致性模型 讲了分布式问题与数据取舍;本章回答:要不要拆、拆成什么形态。盲目 微服务 会把 CAP、BASE 与一致性模型 中的 CAP、部分失败 成本成倍放大。

学完你能
  • 区分分层单体与模块化单体,并说明 ShopFlow 早期可留在单体内的理由
  • 口述 SOA → ESB → 微服务的演进脉络与各自痛点
  • 用康威定律把服务边界与团队拓扑对齐,并列出「不宜拆微服务」的信号
本章先记住 3 件事
  • 形态是手段:先业务边界与交付能力,再选单体还是微服务;勿为简历拆服务。
  • 模块化单体:在单进程内用模块边界练拆分,往往比「先拆再补边界」便宜。
  • 团队即架构:沟通结构会刻在系统里(康威定律);限界上下文 应与能独立交付的团队大致同构。

分层单体与模块化单体的边界

分层单体(Layered Monolith):按技术层划分包结构,所有业务能力共享同一部署单元与数据库。优点是上手快、本地调试简单、事务在进程内完成;风险是边界模糊时演变成「大泥球」——改订单逻辑可能误触库存表。

典型分层(概念)

分层单体 · 请求自上而下
flowchart TB
  UI[Web / API Controller] --> APP[Application Service]
  APP --> DOM[Domain Model]
  DOM --> INF[Infrastructure / JPA]
  INF --> DB[(单一数据库)]

所有业务共享一个 DB 与一次发布;适合团队小、领域尚在高速试错阶段。

模块化单体(Modular Monolith)

仍在一个进程、一次部署,但按业务能力或限界上下文划分子模块(Maven/Gradle 多模块),模块间通过显式 API 通信,禁止随意跨模块访问对方表。

  • shopflow-order、shopflow-inventory、shopflow-payment 模块,共享 shopflow-app 启动器。
  • 模块内可继续分层;模块间仅暴露接口(Java interface / 应用服务门面)。
  • 为将来拆成独立服务做「排练」:边界清晰后再加网络与注册发现。
大泥球单体
  • 按技术层分包,订单 Service 直接注入库存 Repository
  • 无模块依赖规则,循环依赖普遍
  • 「先上线再说」,领域语言在代码里混杂
模块化单体
  • 按 Order / Inventory / Payment 划模块,跨模块只走公开 API
  • ArchUnit / 模块依赖图约束「inventory 不得依赖 order 内部实体」
  • 单库可多 schema 或表前缀,但所有权按模块划分

代码对比:跨边界访问

java
// 反例:订单服务直接操作库存表
@Service
class OrderService {
  @Autowired InventoryRepository inventoryRepo; // 跨上下文泄漏
  void placeOrder(...) {
    inventoryRepo.decrement(skuId, qty);
  }
}
java
// 推荐:通过 inventory 模块公开门面
@Service
class OrderService {
  private final InventoryFacade inventory;
  void placeOrder(...) {
    inventory.reserve(new ReserveCommand(orderId, skuId, qty));
  }
}

SOA、ESB 与微服务的演进脉络

2000 年代前后,面向服务架构(SOA) 强调将企业能力封装为可复用服务,常配合企业服务总线(ESB) 做路由、转换、编排与集中治理。

演进时间线(简化)

企业架构风格演进
timeline
  title 从集中到分布式的常见路径
  2000s : 分层单体 / 大泥球
  2005-2015 : SOA + ESB 集中编排
  2014+ : 微服务 + 轻量消息总线 / API 网关
  近年 : 模块化单体复兴 + 选择性微服务

不是「微服务取代一切」,而是按组织成熟度在谱系上移动。

SOA + ESB 的典型拓扑

ESB 居中编排
flowchart LR
  O[订单系统] --> ESB[ESB 总线]
  I[库存系统] --> ESB
  P[支付系统] --> ESB
  ESB --> T[转换 / 路由 / 编排规则]

所有集成逻辑汇聚到总线:强大但易成瓶颈与「分布式大泥球」的控制中心。

微服务的关键转向

  • 智能端点, dumb pipe:业务逻辑在服务内;管道尽量简单(HTTP/gRPC、消息中间件),避免 ESB 里堆满业务规则。
  • 去中心化数据:每服务自有数据库(或 schema),不再默认「全企业一个库」。
  • 独立部署:团队拥有从设计到运行的全生命周期(DevOps)。
  • ESB 团队成为变更瓶颈,排期长
  • 编排逻辑藏在配置里,难以测试与版本化
  • 技术栈被总线产品绑定
  • 服务间契约(OpenAPI / 事件 schema)+ 消费者驱动测试
  • API 网关 做横切(路由、鉴权),业务编排尽量在域内
  • Spring Cloud Gateway + 注册发现,替代「胖 ESB」

微服务收益、成本与适用场景

微服务不是「更先进的单体」,而是用运维与组织复杂度交换独立演进能力。下表帮助你在评审会上说清利弊。

维度收益成本 / 风险
交付 团队按服务独立发布,减少「全站回归 2 周」 集成测试、契约测试、环境矩阵变多
技术 可按服务选型(如搜索用 Elasticsearch) 统一可观测、安全基线、依赖治理难度上升
弹性 故障隔离(inventory 挂了不必拖垮整站) 分布式事务、Saga、幂等、链路追踪必须到位
组织 与 康威定律 对齐,责任清晰 需要平台工程、SRE、API 治理投入

场景决策表

信号更倾向说明
团队 < 10 人,领域边界每周变 模块化单体 先固化边界,再付分布式税
3+ 团队并行,发布互相阻塞 微服务(边界已清) 独立部署回报大于集成成本
强一致跨库事务是常态 暂缓拆分或保留单体事务 拆后须 Saga / 2PC,复杂度高
读多写少、可接受最终一致 微服务 + 事件 与 CAP、BASE 与一致性模型 AP 路径一致
ShopFlow 示意

当 Order / Inventory / Payment 由不同小队维护、且库存接口变更每周阻塞订单发布时,拆成三个可部署服务才有明显回报;否则模块化单体 + 清晰模块 API 往往足够。

康威定律与团队拓扑

康威定律(Conway's Law):设计系统的组织,其产生的设计等价于组织内的沟通结构。换言之一一你的架构图最终会像你的组织架构图。

对微服务拆分的含义

  • 若订单与库存由同一小队维护,硬拆成两个远程服务只会增加沟通成本,而不会 magically 变快。
  • 若支付由合规独立团队负责,payment-service 作为独立部署单元更自然。
  • 平台团队提供注册发现、网关、CI 模板,让业务小队专注域逻辑(Team Topologies 中的 Platform / Enabling)。
ShopFlow · 团队与服务对齐(目标态)
flowchart TB
  subgraph teamOrder [订单小队]
    OS[order-service]
  end
  subgraph teamInv [库存小队]
    IS[inventory-service]
  end
  subgraph teamPay [支付小队]
    PS[payment-service]
  end
  subgraph platform [平台小队]
    GW[API Gateway]
    REG[注册中心]
  end
  GW --> OS
  GW --> IS
  OS -->|同步 reserve| IS
  PS -->|PaymentCompleted 事件| OS

服务边界与「能独立决策、独立发布」的团队边界尽量同构;跨团队集成走稳定契约。

反模式:组织未拆,服务先拆

两个团队仍共用一个 backlog,却维护六个微服务 — 变更仍需全员协调,却背上分布式排障成本。这叫分布式单体(领域建模、边界上下文与 API 契约 会进一步约束边界)。

何时不应拆微服务

下列信号出现越多,越应推迟物理拆分,优先模块化单体或合并服务。

  • 边界不清:Order 与 Inventory 实体互相引用,无法一句话说清谁拥有哪张表。
  • 无自动化:没有 CI/CD、容器化、集中日志与链路追踪,却要先上 8 个服务。
  • 事务密集:大量跨模块强一致 ACID 需求,拆后只能上重型分布式事务。
  • 团队过小:5 人维护 12 个服务,人均运维负担超过业务开发。
  • 为技术而拆:「微服务很潮」但流量与组织规模仍适合单体。

渐进路径

  1. 模块化单体 — 在进程内练边界与契约。
  2. 绞杀者(Strangler) — 新能力在新服务,旧模块逐步下线(演进模式、反模式与架构评审 再展开)。
  3. 选择性拆分 — 先拆变化最快或需独立扩缩的上下文(如 Inventory 读多写多)。
先想 10 秒:ShopFlow 只有 6 人,是否应立即拆成 order / inventory / payment 三个独立部署?

通常否:优先模块化单体 + 清晰门面;当发布阻塞或团队分组成熟后再拆。拆早会把 CAP、BASE 与一致性模型 中的 CAP、部分失败成本全部兑现且无人力消化。

下一章进入 领域建模、边界上下文与 API 契约,把「模块/服务边界」落到限界上下文与 OpenAPI。

本章结论

每条对应上一节 h3,用于架构评审前的 30 秒复述。

  • 单体谱系:分层单体按技术分包;模块化单体按业务能力/限界上下文分模块,单进程单部署,是拆微服务前的低成本排练。
  • 演进脉络:SOA+ESB 集中编排易成瓶颈;微服务强调智能端点、简单管道与去中心化数据,Spring Cloud 网关等承担横切而非「胖总线」。
  • 利弊权衡:微服务换独立发布与技术异构,付分布式事务、可观测与集成测试代价;边界未清时收益为负。
  • 康威定律:服务边界应与能独立交付的团队拓扑对齐;组织未拆却服务先拆,往往得到分布式单体。
  • 不宜硬拆:边界模糊、无自动化、事务密集、团队过小或为简历拆服务时,优先模块化单体或绞杀者渐进迁移。

复习与自检

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

0 / 5 已勾选

本章速查卡
概念一句
模块化单体单部署,模块间只走公开 API
ESB集中编排,易成瓶颈
微服务独立部署 + 自有数据 + 组织对齐
康威定律架构镜像沟通结构
不宜拆边界糊、无自动化、团队过小
答辩 / 面试口述题
  1. 模块化单体与微服务的边界在哪里?何时从前者演进到后者?
  2. 为什么说「ESB 把集成逻辑集中」在微服务时代常被 Gateway + 域内编排替代?
  3. 举 ShopFlow 一个「不应立即拆微服务」的信号并说明缓解步骤。

下一步:勾选过关清单、完成测验与 Demo;下一章 领域建模、边界上下文与 API 契约 固化限界上下文与 API 契约。

动手练习

用「架构形态决策表」与康威定律,为 ShopFlow(7 人 / 日单 200)写架构选型备忘。

操作步骤

  1. 按 Mermaid「模块化单体模块依赖」画出三模块,标允许依赖(→ api)与禁止依赖(× 跨模块 Entity)。
  2. 填写「单体 vs 微服务决策表」ShopFlow 当前阶段一行:团队规模、部署频率、数据边界,勾选推荐形态。
  3. 用两句话对比 ESB 集中转换 vs Gateway「dumb pipe」在变更谁审批上的差异。

判断练习

  1. 「为用上 K8s,先把 Order 拆成 5 个微服务,团队仍是一个 7 人全栈小队。」违反哪 2 条原则?更稳妥的第一步?

    参考答案

    违反康威定律(组织未拆、服务先拆)与不宜硬拆信号(边界/自动化不足)。第一步:模块化单体 + ArchUnit 约束。

  2. 哪条更像 SOA+ESB 反模式?① Gateway 只路由鉴权 ② 所有 JSON↔XML 转换集中在 ESB ③ 各服务维护 OpenAPI

    参考答案

    答:②。ESB 成为转换与编排瓶颈,变更需总线团队审批;①③ 符合智能端点 + 契约分散维护。

Demo:架构形态与团队拓扑实验室

目录:demos/basics-03-architecture-styles-lab/。验收:团队拓扑表三行与决策表「约束」列已填完。

已完成领域建模、边界上下文与 API 契约

约 30 分钟 · 阶段:分布式基石 · 能力:划分限界上下文并设计可演进的 REST/OpenAPI 契约

为什么要学本章

架构风格:单体、SOA 与微服务 选了「单体还是微服务」;本章回答边界画在哪、对外说什么语言。没有清晰的 限界上下文 与 OpenAPI 契约,拆服务只会得到多个互相渗透的「小泥球」——集成靠口头约定,改接口即全站停摆。

学完你能
  • 用 DDD 战略设计说明 ShopFlow 的 Order / Inventory / Payment 三大上下文与子域类型
  • 对比「按业务能力拆分」与「按技术层拆分」,并识别模型泄漏信号
  • 设计 REST 资源 URI、版本策略与统一错误码,并理解契约优先与 消费者驱动契约 的价值
本章先记住 3 件事
  • 上下文即边界:每个限界上下文有独立统一语言与模型;跨上下文只通过显式 API/事件,禁止直接读对方表。
  • 契约先于实现:OpenAPI 是团队间「法律文本」;改契约须版本化与消费者协商,而非偷偷改 JSON 字段。
  • ShopFlow 三上下文:Order 管订单生命周期;Inventory 管预留/扣减与防超卖;Payment 管支付与退款——各管各的数据。

DDD 战略设计:限界上下文与子域

领域驱动设计(DDD)战略设计关注「大问题如何切」:识别子域(Subdomain)类型,并为每个核心能力划定限界上下文(Bounded Context)。

子域三分法

类型含义ShopFlow 示例
核心域 差异化竞争力,值得自研投入 订单编排、库存防超卖策略
支撑域 必需但非差异化,可适度定制 支付对接、物流通知
通用域 行业通用能力,优先买/用 SaaS 短信验证码、对象存储

限界上下文的关键属性

  • 统一语言(Ubiquitous Language):上下文内术语一致;Order 里的 OrderLine 与 Inventory 里的 StockReservation 不必强行合并。
  • 模型边界:实体、聚合根、仓储仅在本上下文内完整;对外只暴露 DTO / 应用服务 / 事件。
  • 与部署单元的关系:理想上一上下文对应一个可独立演进的服务(或模块化单体中的一个模块),与 康威定律 对齐。
ShopFlow · 三大限界上下文(战略视图)
flowchart LR
  subgraph orderCtx [Order 上下文]
    O[Order 聚合]
    OL[OrderLine]
  end
  subgraph invCtx [Inventory 上下文]
    S[Sku / Stock]
    R[Reservation]
  end
  subgraph payCtx [Payment 上下文]
    P[Payment]
    Ref[Refund]
  end
  orderCtx -->|ReserveCommand API| invCtx
  orderCtx -->|InitiatePayment API| payCtx
  payCtx -->|PaymentCompleted 事件| orderCtx

箭头表示集成关系(API 或事件),不是共享数据库。各上下文内部模型互不直接引用。

模型泄漏
  • Order 实体里嵌套 Inventory 的 Warehouse 字段
  • 跨上下文 JOIN 同一张宽表
  • 改库存字段导致订单编译失败
清晰边界
  • Order 只持有 skuId、qty 等集成所需最小字段
  • 库存详情通过 GET /skus/{id} 按需查询
  • 上下文间契约版本化,内部重构不影响对方

按业务能力拆分 vs 按技术层拆分

架构风格:单体、SOA 与微服务 已对比分层单体与模块化单体;本节从 DDD 视角强调:微服务边界应沿业务能力(限界上下文)切,而非沿 Controller / Service / Repository 等技术层切。

两种切法对比

维度按技术层拆分按业务能力 / 限界上下文拆分
包/服务结构 user-controller、order-controller 各一服务,共享同一 DB order-service、inventory-service,各管各的数据
变更影响 改订单逻辑仍可能动「公共 order 表」 订单模型变更局限在 Order 上下文内
团队对齐 按「前端组 / 后端组」分工,与业务脱节 订单小队拥有 Order 上下文全栈
分布式代价 背上网络调用却无独立数据边界 → 分布式单体 真正独立部署与演进,但须设计 API/事件与一致性

代码对比:集成方式

java
// 反例:按「层」拆服务,却共享 order 表与实体
// order-query-service 直接读 order 表 join inventory 表
@Query("SELECT o, i FROM Order o JOIN Inventory i ON ...")
List<OrderDetailDto> findAllWithStock();
java
// 推荐:Order 上下文通过契约调用 Inventory
public OrderView getOrder(UUID orderId) {
  Order order = orderRepo.findById(orderId);
  StockSnapshot stock = inventoryClient.getSku(order.getSkuId());
  return OrderView.from(order, stock); // 组合视图,非共享实体
}
实践提示

模块/服务内仍可分层(controller → application → domain → infra);拆分维度是业务能力,不是禁止分层。

REST 资源建模、版本化与错误码规范

限界上下文对外通常以 RESTful HTTP API 暴露能力(practice 阶段亦用 OpenFeign 消费)。好的资源建模让调用方「读 URI 即懂语义」,并预留演进空间。

资源 URI 原则

  • 名词复数、表意资源:/orders、/skus/{skuId}/reservations,避免 /createOrder 动词路径。
  • 动作用语义子资源或 PATCH:取消订单 POST /orders/{id}/cancel 或 PATCH /orders/{id} + {"status":"CANCELLED"}。
  • 跨上下文 ID 引用:Order 只存 skuId,不在 URI 里嵌 inventory 内部仓库 ID,除非该 ID 已是稳定集成标识。
  • 幂等键:写操作(如 POST /reservations)要求 Header Idempotency-Key 或 body 内 requestId,应对 部分失败 重试。

ShopFlow 核心端点(示意)

上下文方法路径说明
OrderPOST/api/v1/orders创建订单
OrderGET/api/v1/orders/{orderId}查询订单
InventoryPOST/api/v1/reservations预留库存(幂等)
InventoryGET/api/v1/skus/{skuId}查询 SKU 可售量
PaymentPOST/api/v1/payments发起支付

版本化策略

  • /api/v1/orders → /api/v2/orders:直观,网关路由简单
  • 旧版保留 deprecate 周期,文档标明下线日
  • ShopFlow 入门推荐:路径前缀 /api/v1
  • Accept: application/vnd.shopflow.order.v2+json
  • 或 Header Api-Version: 2
  • URI 不变,适合公开 API 长期兼容;网关配置稍复杂

统一错误响应

json
{
  "code": "INVENTORY_INSUFFICIENT",
  "message": "SKU-1001 可售库存不足",
  "traceId": "7f3a2b1c-...",
  "details": { "skuId": "SKU-1001", "requested": 5, "available": 2 }
}
  • code:机器可读、文档化枚举(各服务可前缀 ORDER_ / INV_,网关可映射为统一表)。
  • traceId:与日志、链路追踪关联,便于排障。
  • HTTP 状态码表达协议层语义(404 资源不存在、409 冲突、422 业务校验失败),body 的 code 表达业务层语义。

契约优先:OpenAPI 与消费者驱动契约

契约优先(Contract First):先写 OpenAPI 规范,再生成/实现 Controller 与客户端桩代码。这样 Order 与 Inventory 团队在编码前就对齐字段、错误码与幂等语义。

OpenAPI 片段(Inventory · 预留库存)

yaml
openapi: 3.1.0
info:
  title: ShopFlow Inventory API
  version: 1.0.0
paths:
  /api/v1/reservations:
    post:
      operationId: createReservation
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [orderId, skuId, quantity]
              properties:
                orderId: { type: string, format: uuid }
                skuId: { type: string }
                quantity: { type: integer, minimum: 1 }
      responses:
        '201':
          description: 预留成功
        '409':
          description: 库存不足或冲突

契约优先工作流

  1. 领域专家 + 开发在 PR 中评审 openapi/inventory-api.yaml。
  2. CI 校验 breaking change(如删除字段、改类型)。
  3. 实现方对照契约写 Spring MVC / WebFlux;消费方用 OpenFeign + 生成 DTO 或手写映射。
  4. 契约测试(Pact 等)在 CI 验证「消费者期望 ⊆ 提供者实现」。

消费者驱动契约(CDC)

在 微服务 集成中,调用方(消费者) 定义「我需要哪些字段与状态码」,写入契约文件;提供方 在 CI 中验证能否满足。这样 inventory 重构内部表结构时,只要仍满足 order-service 的契约,集成测试即可通过。

契约优先 + CDC 协作
sequenceDiagram
  participant OC as order-service 消费者
  participant Pact as 契约仓库 / CI
  participant IS as inventory-service 提供者
  OC->>Pact: 提交期望契约(reserve 201/409)
  IS->>Pact: Provider 验证任务
  Pact->>IS: 验证通过 / 失败
  Note over OC,IS: 实现前对齐;改 API 须双方评审

对比「联调当天才发现字段少了」:契约将集成失败左移到 PR 阶段。

FAQ:OpenAPI 与 Java 代码谁为准?

团队应显式选定真源:契约优先则以 YAML 为准;代码优先则须 CI 反向生成/校验 OpenAPI diff。ShopFlow 教程采用契约优先,与 SpringDoc 可并存(由 YAML 生成或双向校验)。

ShopFlow:订单、库存、支付上下文划分

将本章方法落到 ShopFlow 全课主线:三个上下文各司其职,通过 API 网关 对外统一入口,内部走同步 API + 异步事件。

上下文职责表

上下文核心聚合拥有的数据对外能力
Order Order 订单头、明细、状态机 创建/查询/取消订单;订阅 PaymentCompleted
Inventory Sku、Reservation 可售量、预留记录 预留/释放/扣减;强一致 防超卖
Payment Payment 支付单、渠道回调 发起支付、退款;发布 PaymentCompleted

下单 happy path(集成视图)

ShopFlow · 创建订单(同步 + 事件)
sequenceDiagram
  participant U as 用户
  participant GW as API Gateway
  participant O as order-service
  participant I as inventory-service
  participant P as payment-service
  U->>GW: POST /api/v1/orders
  GW->>O: 转发
  O->>I: POST /reservations (Idempotency-Key)
  I-->>O: 201 Reserved
  O->>P: POST /payments
  P-->>O: 202 Accepted
  O-->>U: 201 Order PENDING_PAYMENT
  P-->>O: PaymentCompleted 事件
  O->>O: 状态 → PAID

Order 编排流程;Inventory 与 Payment 不直接互调。失败补偿在 分布式数据:事务、幂等与 Saga(Saga)展开。

上下文映射(Context Map)简图

  • Order → Inventory:Customer-Supplier(Order 为客,Inventory 为供);Inventory 上游发布 OpenAPI,Order 消费。
  • Order → Payment:同上;Payment 完成后以事件通知 Order(避免 Payment 同步回调 Order 造成环依赖)。
  • Anti-corruption Layer:Order 内 InventoryClient 将外部 DTO 转为域内 ReservationResult,隔离外部模型变化。
先想 10 秒:能否让 Payment 服务直接 UPDATE order 表的 status 字段?

不应:违反上下文数据所有权。应通过 API 或事件让 Order 上下文自行变更状态;跨库写是 分布式单体 典型信号。事件可靠投递可结合 Outbox(CAP、BASE 与一致性模型 预览,Kafka 事件驱动与 Redis 缓存 展开)。

下一章 通信模式:同步、异步与事件驱动 将把同步/异步、编排与编舞、Outbox 与本章契约映射到 Spring Cloud 概念。

本章结论

每条对应上一节 h3,用于评审前 30 秒复述。

  • 战略 DDD:用子域分类投入优先级;限界上下文划定统一语言与模型边界,跨上下文禁止共享实体与随意 JOIN。
  • 拆分维度:按业务能力/限界上下文切服务或模块,而非按 Controller 等技术层;否则易成分布式单体。
  • REST 规范:资源 URI、路径版本、统一错误码与幂等键;HTTP 状态与业务 code 分工明确。
  • 契约优先:OpenAPI 先行 + CI 防 breaking change;CDC 让消费者定义期望,集成失败左移到 PR。
  • ShopFlow 三上下文:Order 编排、Inventory 防超卖、Payment 收单;网关统一入口,Payment 以事件回传 Order。

复习与自检

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

0 / 5 已勾选

本章速查卡
概念一句
限界上下文独立语言与模型;对外只暴露 API/事件
按能力拆分Order/Inventory/Payment,非按 Controller 拆
契约优先OpenAPI 先评审再编码
CDC消费者定义期望,提供者 CI 验证
幂等键写操作 Header Idempotency-Key
答辩 / 面试口述题
  1. 什么是模型泄漏?举 ShopFlow 一个反例与一个推荐做法。
  2. URI 版本与 Header 版本各适合什么场景?
  3. 为何 Payment 不应直接更新 order 表?应如何集成?

下一步:勾选过关清单、完成测验与 Demo;下一章 通信模式:同步、异步与事件驱动 讲同步/异步通信与 Outbox 预览。

动手练习

产出 Inventory 最小 OpenAPI 与三上下文映射图,对照错误码与幂等头约定自检。

操作步骤

  1. 按「REST 错误码约定」表,为 POST /api/v1/reservations 补全 409 库存不足与幂等冲突的 JSON body(含 code、message)。
  2. 在 YAML 中添加 Idempotency-Key header 参数,与正文 OpenAPI 片段对齐。
  3. 绘制三上下文映射图,标 Customer-Supplier 边与 PaymentCompleted 事件方向。

判断练习

  1. 哪条 API 设计违反契约?① Inventory 暴露 PATCH /orders/{id}/status ② Order 调 POST /reservations 带幂等键 ③ Payment 回调用 callbackToken 验签

    参考答案

    答:①。订单状态属 Order 上下文,Inventory 不应直接改 orders;应通过 API/事件让 Order 自行变更。②③ 符合契约与幂等/验签约定。

  2. OpenAPI 缺少 Idempotency-Key header,却写「支持重复提交安全」。违反哪条约定?应改哪一段?

    参考答案

    违反契约优先与预留接口幂等约定。应在 OpenAPI parameters 增加 header 定义,并在 POST 说明中引用;实现与文档保持一致。

Demo:限界上下文与 API 契约实验室

目录:demos/basics-04-domain-api-lab/。验收:YAML 含 reservations 路径 + 响应码 201/409/422。

已完成通信模式:同步、异步与事件驱动

约 32 分钟 · 阶段:分布式基石 · 能力:为 ShopFlow 选型同步/异步通信并理解编排与 Outbox

为什么要学本章

前四章定了边界与契约;本章回答服务之间怎么说话。选错模式会把 部分失败 放大成长链路阻塞,或把本该异步的通知硬拉成同步耦合。

学完你能
  • 为 ShopFlow 各集成点选择 REST/OpenFeign、gRPC 或消息,并说明理由
  • 对比编排与编舞,识别 Order 下单链适合哪种
  • 说明 幂等键、去重与 Outbox 如何配合异步事件
  • 将本章概念映射到 Spring Cloud 实战章节(OpenFeign、Kafka、Gateway)
本章先记住 3 件事
  • 同步要短:需要立即知道成败的步骤(预留库存)用请求/响应;链路过长须熔断、超时与幂等。
  • 异步解耦:支付结果、通知类用事件 + 消费者组;接受 最终一致,靠幂等与对账收敛。
  • 发消息要可靠:「写库 + 发 Kafka」用 Outbox,避免双写不一致。

请求/响应、RPC 与 gRPC 适用场景

请求/响应(Request/Response)是分布式中最直观的模式:调用方发请求、阻塞或异步等待响应。在 Java 微服务中常体现为 REST + OpenAPI 契约,practice 阶段用 OpenFeign 消费。

三种同步风格对比

风格特点ShopFlow 示例
REST/HTTP + JSON 通用、易调试、网关友好;性能略低于二进制 RPC order → inventory 预留、查询 SKU
RPC(如 gRPC) 强类型契约、HTTP/2、流式;需生成桩代码 高 QPS 内部调用、多语言团队(可选)
GraphQL / BFF 聚合 前端一次取多资源;聚合层负责扇出 商品详情页聚合(性能、容量与数据扩展 再展开)
同步滥用
  • order → inventory → payment → 通知 全同步链
  • 无超时,线程阻塞堆积
  • 下游慢一步拖垮整站
同步克制
  • 仅「须当场确认」的步骤同步(预留库存)
  • 显式超时 + 有限重试 + 幂等键
  • 支付结果改走事件(见下一节)

OpenFeign 调用预览(YAML)

yaml
# 详见 OpenFeign、负载均衡与服务间调用;此处仅示意超时与幂等头
spring:
  cloud:
    openfeign:
      client:
        config:
          inventory-service:
            connectTimeout: 1000
            readTimeout: 2000
# 调用时 Header: Idempotency-Key: {orderId}

消息队列、发布订阅与事件通知

当调用方不必立即知道下游处理结果,或需要削峰、解耦多订阅者时,使用异步消息。ShopFlow 中 PaymentCompleted、OrderCreated 等领域事件适合走 Kafka 等 MQ(详见 Kafka 事件驱动与 Redis 缓存)。

核心概念

  • 消息队列(Queue):点对点,一条消息通常被一个消费者处理(任务分发)。
  • 发布/订阅(Pub/Sub):一条事件可被多个订阅者各自处理(订单创建 → 库存、积分、通知各订一份)。
  • 领域事件:用过去式命名(OrderCreated、PaymentCompleted),载荷含最小集成字段 + 事件 ID。
  • 消费者组:同组内竞争消费保证扩展;不同组各自完整消费一遍。
ShopFlow · PaymentCompleted 发布订阅
flowchart LR
  Pay[payment-service] -->|publish| K[(Kafka topic)]
  K --> G1[order-service 消费者组]
  K --> G2[analytics 消费者组]
  G1 --> O[更新订单 PAID]
  G2 --> A[报表入库]

order 与 analytics 各用消费者组,互不影响进度;须幂等处理重复投递。

  • 须立即向用户反馈成败(预留库存、校验优惠券)
  • 链路短、调用方少、QPS 可控
  • 可接受 部分失败 语义已设计(幂等 + 查询)
  • 支付回调、发货通知、积分累计
  • 多下游订阅同一业务事实
  • 可接受秒级 最终一致

编排 vs 编舞(Choreography)

跨多个服务的业务流程(如下单 → 预留 → 支付 → 确认)如何协调?两种主流思路:编排(Orchestration) 由中央协调者按脚本推进;编舞(Choreography) 各服务监听事件、自行决定下一步,无中心大脑。

对比表

维度编排编舞
协调者 order-service 或独立 orchestrator 调用各步 各服务订阅事件,本地状态机驱动
可见性 流程集中在一点,易画序列图 流程分散,须链路追踪串联
耦合 协调者知悉所有步骤 API 服务只关心相关事件,添加订阅者较易
失败处理 常配合 Saga 补偿,由协调者触发 每服务自行补偿或发补偿事件
ShopFlow · 编排式下单(简化)
sequenceDiagram
  participant O as order-service 编排者
  participant I as inventory-service
  participant P as payment-service
  O->>I: 同步 reserve
  I-->>O: 201
  O->>P: 同步 initiate payment
  P-->>O: 202
  Note over O: 等待 PaymentCompleted 事件
  P-->>O: event PaymentCompleted
  O->>O: 状态 PAID

ShopFlow 入门采用「order 编排 + 支付异步事件」混合:关键同步、通知异步。

选型提示

步骤 ≤ 3 且团队小:order 内编排往往足够。步骤多、多团队自治:可逐步引入编舞,但须统一事件 schema 与可观测性。勿在 API 网关 里写编排逻辑。

幂等键、去重与 Outbox 模式预览

异步与重试使「至少一次投递」成为常态。须从生产端与消费端双侧保证正确性。

消费端:幂等与去重

  • 幂等键:同步写操作 Header Idempotency-Key;异步用 eventId 或业务键。
  • 去重表:消费者处理前插入 processed_events(event_id),重复则跳过。
  • 业务幂等:如「订单已 PAID 则忽略重复 PaymentCompleted」。

生产端:Transactional Outbox

问题:订单入库与发 Kafka 两个操作无法共用一个本地事务。 Outbox 将待发事件与业务写入同一数据库事务,后台投递器异步发到 MQ。

java
@Transactional
void createOrder(CreateOrder cmd) {
  Order order = orderRepo.save(new Order(cmd));
  outboxRepo.save(new OutboxEvent(
    "OrderCreated",
    order.getId().toString(),
    toJson(new OrderCreatedPayload(order.getId(), ...))
  ));
  // 同一事务;投递器稍后 poll outbox → Kafka
}
先想 10 秒:消费者收到两次相同 eventId 的 PaymentCompleted,应如何处理?

第二次应识别为重复:查去重表或订单状态已为 PAID,直接 ack 不再改库。若仍重复扣减或重复发券,说明缺少消费端幂等。

为 Spring Cloud 实战做概念映射

basics 阶段原理收官。下表将本章概念映射到 practice 阶段 ShopFlow 工程(Java 21 + Spring Boot 3.5 + Spring Cloud 2025)。

本章概念Spring Cloud / 组件实战章节
同步 REST + 契约 OpenFeign + LoadBalancer OpenFeign、负载均衡与服务间调用
统一入口、路由 Spring Cloud Gateway 服务注册发现与 API 网关
注册发现 Eureka / Nacos 服务注册发现与 API 网关
领域事件、Pub/Sub Spring Cloud Stream + Kafka Kafka 事件驱动与 Redis 缓存
Outbox、跨服务一致 本地事务 + 投递器 / Saga 分布式数据:事务、幂等与 Saga
脚手架、BOM Spring Boot 3.5 + Cloud BOM Spring Boot 3.5 与 Spring Cloud 脚手架
阶段一(basics)完成

你已具备分布式问题域、CAP、架构形态、DDD 边界、API 契约与通信选型基础。下一阶段在 ShopFlow 仓库中动手搭服务,建议从 Spring Boot 3.5 与 Spring Cloud 脚手架 开始。

本章结论

每条对应上一节 h3,basics 阶段收官复述。

  • 同步通信:REST/OpenFeign 适合须当场确认的步骤;链宜短,配超时、幂等与熔断;gRPC 等留给高性能内部场景。
  • 异步事件:Pub/Sub + 消费者组解耦支付通知等多订阅者;接受最终一致,消费端必须幂等。
  • 编排 vs 编舞:ShopFlow 入门用 order 编排 + 事件混合;编舞适合多团队自治但需统一事件契约与追踪。
  • 可靠投递:幂等键 + 去重表 + Outbox 解决重试与「写库发消息」原子性;与 CAP、BASE 与一致性模型 BASE 路径一致。
  • 实战映射:见上一节 Spring Cloud 映射表;下一阶段从 Spring Boot 3.5 与 Spring Cloud 脚手架 起步。

复习与自检

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

0 / 5 已勾选

本章速查卡
概念一句
同步当场要结果;宜短链 + 幂等
异步事件解耦;消费幂等
编排中央协调多步
编舞各服务听事件自行反应
Outbox写库与待发事件同事务
答辩 / 面试口述题
  1. 为什么 PaymentCompleted 用 Kafka 而不是 order 同步轮询 payment?
  2. Outbox 与「先 commit 再发消息」有何区别?
  3. 编舞模式下如何排查「订单卡在未支付」?

下一步:勾选过关清单、完成测验与 Demo;进入 practice 阶段,从 Spring Boot 3.5 与 Spring Cloud 脚手架 搭建 ShopFlow 工程。

动手练习

填写同步/异步选型与 Spring Cloud 映射表,并解释 Outbox 与 createOrder 代码块关系。

操作步骤

  1. 复制「PaymentCompleted 发布订阅」Mermaid,在 order、analytics 消费者旁各写一句消费端幂等策略。
  2. 为「编排式下单」序列图中 reserve、initiate payment、等事件三步各标 sync/async。
  3. 对照 createOrder + OutboxEvent 片段:去掉 outbox 直接发 Kafka 会破坏哪条 BASE 配套手段?

判断练习

  1. 「order→inventory→payment→短信→积分」全 OpenFeign 同步且无超时。最先应改哪一处?

    参考答案

    支付改异步事件(或至少缩短链并设超时)。支付结果不必阻塞 order 线程;全同步长链最易触发「同步滥用」中的线程堆积与级联失败。

  2. PaymentCompleted 被 order 与 analytics 分别消费——编舞还是编排?新增 analytics 订阅者应改 order 还是只加消费者?

    参考答案

    analytics 侧属编舞(听事件自行处理)。新增 analytics 订阅者只加 Kafka 消费者,无需改 order 编排代码,这是编舞扩展性优势。

Demo:通信模式选型实验室

目录:demos/basics-05-communication-patterns-lab/。验收:集成点选型表 3 行均已填模式 + 理由。

已完成Spring Boot 3.5 与 Spring Cloud 脚手架

约 35 分钟 · 阶段:Java 微服务栈实战 · 能力:搭建 ShopFlow 多模块父工程并本地启动首个服务

为什么要学本章

basics 阶段定了边界与通信;practice 从可运行的工程骨架开始。版本对齐(Boot + Cloud BOM)、模块分层与 Profile 约定若在一开始做错,后续注册发现、Feign、Kafka 会在依赖冲突与配置漂移上反复踩坑。

学完你能
  • 用 JDK 21 + Maven 导入 Spring Cloud BOM,与 Spring Boot 3.5 对齐
  • 按 api / service / common 拆分 ShopFlow 多模块工程
  • 约定 application.yml、Profile 与本地启动顺序
  • 启用 Actuator 健康检查与统一 JSON 日志格式
本章先记住 3 件事
  • BOM 管版本:Cloud 组件版本由 spring-cloud-dependencies 统一管理,子模块勿手写版本号。
  • 模块边界:api 放 DTO/契约,service 可运行,common 放跨服务工具;与 限界上下文 一一对应。
  • 先单体模块、后拆进程:本章先跑通 order-service;全链路注册发现见 服务注册发现与 API 网关。

JDK 21、Maven BOM 与 Spring Cloud 2025 对齐

本课栈:Java 21 LTS、Spring Boot 3.5.x、Spring Cloud 2025.0.x(2025.1 发布列车)。Spring Cloud 采用发布列车(Release Train)命名:2025.0.1 与 Boot 3.5 在官方兼容矩阵中配对,子项目通过 BOM import 继承版本。

父 POM:Boot 父 + Cloud BOM

xml
<!-- shopflow/pom.xml 片段 -->
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.5.0</version>
</parent>

<properties>
  <java.version>21</java.version>
  <spring-cloud.version>2025.0.1</spring-cloud.version>
</properties>

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>${spring-cloud.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>
版本散落
  • 各 service 的 pom 手写 openfeign 版本
  • Boot 3.4 与 Cloud 2025 混用
  • CI 与本地 JDK 不一致(仍用 17)
BOM 统一
  • 父 POM 一处定义 spring-cloud.version
  • 子模块依赖不写 <version>
  • .mvn/jvm.config 或 CI 固定 JDK 21
FAQ:能否只用 Spring Boot、暂不引 Cloud?

可以跑单个 REST 服务,但 ShopFlow 实战依赖注册发现、Gateway、OpenFeign。本章仍引入 Cloud BOM,以便下一章直接加 spring-cloud-starter-netflix-eureka-client 而无需改版本矩阵。

多模块工程:api / service / common 分层

将 领域建模、边界上下文与 API 契约 中的 ShopFlow 三上下文落到 Maven 模块:每个上下文至少 api + service,共享能力放入 common。这与 模块化单体 的分模块思路一致,只是 service 模块将独立进程部署为 微服务。

ShopFlow 多模块拓扑(本章目标范围)
flowchart TB
  subgraph parent[shopflow-parent]
    C[shopflow-common]
    OA[order-api]
    OS[order-service]
    IA[inventory-api]
    IS[inventory-service]
    PA[payment-api]
    PS[payment-service]
  end
  OS --> OA
  OS --> C
  IS --> IA
  IS --> C
  PS --> PA
  PS --> C

service 依赖对应 api + common;service 之间禁止直接依赖对方 implementation,跨上下文走 HTTP/事件(见 basics 契约)。

各层职责

模块后缀职责ShopFlow 示例
-api DTO、Feign 接口、OpenAPI 生成物;无 Spring 运行时 CreateOrderRequest、OrderApi
-service @SpringBootApplication、Controller、领域逻辑、持久化 order-service 端口 8081
common 错误码枚举、JSON 工具、日志 MDC 助手 ShopFlowErrorCodes

order-service 最小依赖

xml
<!-- shopflow-order-service/pom.xml -->
<dependencies>
  <dependency>
    <groupId>com.shopflow</groupId>
    <artifactId>shopflow-order-api</artifactId>
  </dependency>
  <dependency>
    <groupId>com.shopflow</groupId>
    <artifactId>shopflow-common</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
  </dependency>
</dependencies>

配置外部化与 Profile 约定

多服务本地联调时,端口、数据源、日志级别必须可切换且可预测。ShopFlow 约定:

  • 默认:application.yml 放共享默认(应用名、Actuator 暴露端点)。
  • 环境:application-local.yml 本地 H2/端口;application-docker.yml 供 Compose(见 practice-02)。
  • 激活:spring.profiles.active=local(IDE Run Configuration 或环境变量)。
  • 敏感项:不进 Git;用 .env / 环境变量占位(生产走 Config Server,见 advanced 章)。
yaml
spring:
  application:
    name: order-service
management:
  endpoints:
    web:
      exposure:
        include: health,info
  endpoint:
    health:
      show-details: when_authorized
yaml
server:
  port: 8081
logging:
  level:
    com.shopflow: DEBUG
先想 10 秒:为何不把 8081 端口写死在 Java 代码里?

同一 artifact 要在本地、CI、容器中使用不同端口与环境;外部化后只需改 Profile 或环境变量,避免重新编译。也与 通信模式 中「契约稳定、部署可变」一致。

ShopFlow 仓库结构与本地启动顺序

推荐 monorepo 根目录 shopflow/,与课程 Demo 对齐。首章只需能启动 order-service 并访问健康检查;inventory / payment 可先留空模块占位。

目录树(精简)

bash
shopflow/
├── pom.xml                      # 父 POM + BOM
├── shopflow-common/
├── shopflow-order-api/
├── shopflow-order-service/
│   └── src/main/java/.../OrderServiceApplication.java
├── shopflow-inventory-api/
├── shopflow-inventory-service/
├── shopflow-payment-api/
└── shopflow-payment-service/

本地启动顺序(本章)

  1. 确认 java -version 为 21;根目录 mvn -q -DskipTests install 编译全模块。
  2. 进入 shopflow-order-service,mvn spring-boot:run -Dspring-boot.run.profiles=local。
  3. 访问 http://localhost:8081/actuator/health,期望 {"status":"UP"}。
  4. (可选)用 IDE 运行 OrderServiceApplication,VM options 加 -Dspring.profiles.active=local。
与后续章节衔接

practice-02 将先启动注册中心,再启动 order / inventory 并注册;本章无需 Eureka,但请保持应用名 spring.application.name 与模块名一致,便于后续服务发现。

健康检查 Actuator 与统一日志格式

Spring Boot Actuator 提供生产就绪端点。本地与 K8s 探针都依赖 /actuator/health;统一 JSON 日志则便于后续接入 ELK 与 traceId(可观测性 章展开)。

健康检查分组(入门)

  • liveness:进程是否存活(默认 health 即可)。
  • readiness:是否可接流量(后续加 DB、注册中心检查)。
  • 勿对外暴露 env、beans 等敏感端点;生产用 Spring Security 或网络策略。

统一 JSON 日志(Logback)

xml
<!-- shopflow-common/src/main/resources/logback-spring.xml 片段 -->
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
  <encoder class="net.logstash.logback.encoder.LogstashEncoder">
    <includeMdcKeyName>traceId</includeMdcKeyName>
    <customFields>{"service":"${spring.application.name:-unknown}"}</customFields>
  </encoder>
</appender>

本章在 common 中引入 logstash-logback-encoder,各 service 自动继承;日志行含 service、预留 traceId MDC 字段,与 Cloud 可观测组件 后续对齐。

Actuator 端点速查
路径用途
/actuator/health存活/就绪(K8s 探针)
/actuator/info构建版本、Git 信息(可选)
/actuator/metricsMicrometer 指标(advanced 章)

本章结论

每条对应上一节 h3,practice 阶段起步复述。

  • 版本对齐:父 POM 用 Boot 3.5 + spring-cloud-dependencies 2025.0.x BOM;JDK 21;子模块不写 Cloud 组件版本。
  • 模块分层:api / service / common 对应限界上下文;service 可独立启动,跨上下文只走 API/事件。
  • 配置约定:application.yml + Profile(local/docker);端口与敏感项外部化。
  • 启动顺序:根目录 install → 单服务 spring-boot:run → 验证 /actuator/health。
  • 可运维基线:Actuator health + 统一 JSON 日志,为注册发现与可观测性铺路。

复习与自检

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

0 / 5 已勾选

下一步:完成 Demo 与测验;继续 服务注册发现与 API 网关,把 order / inventory 注册到 Eureka 并经由 Gateway 统一入口。

动手练习

在本地跑通 order-service,完成父 POM、多模块、Profile 与 Actuator 命令级验收。

操作步骤

  1. 确认 shopflow/pom.xml 含 spring-cloud.version 与 BOM import;在子模块手写 <version> 观察冲突后改回。
  2. 按「多模块拓扑」Mermaid,列出 order-service 允许依赖的 2 个模块,并说明为何不能依赖 inventory implementation。
  3. 配置 application-local.yml 端口,mvn spring-boot:run -Dspring-boot.run.profiles=local,curl -s localhost:8081/actuator/health 期望 UP。
  4. 在 common 添加 JSON Logback encoder,确认日志含 "service":"order-service"。

判断练习

  1. 端口写进 Java 静态字段且无 application-local.yml。两处应改回外部化的位置?违反哪条 Profile 约定?

    参考答案

    删除 Java 硬编码;在 application-local.yml 写 server.port,默认 application.yml 放共享项。违反「环境差异外部化、同一 artifact 多环境复用」。

  2. 子模块 pom 中 OpenFeign 写死 4.0.0,与 BOM 2025.0.1 并存。应删哪段?保留有何风险?

    参考答案

    删除子模块依赖上的 <version>4.0.0</version>,由 BOM 管理。保留可能导致与 Boot 3.5 矩阵不兼容、集成测试通过但生产 classpath 冲突。

Demo:ShopFlow 脚手架起步

目录:demos/practice-01-spring-cloud-bootstrap-lab/。验收:mvn install 成功 + health 为 UP + 改端口后仍可达。

延伸学习

已完成服务注册发现与 API 网关

约 38 分钟 · 阶段:Java 微服务栈实战 · 能力:搭建注册中心、注册 order/inventory 并通过 Gateway 统一入口访问

为什么要学本章

上一章 能单点启动 order-service;多实例、多服务后客户端不能再记 IP:端口。 服务发现 解决「找谁」;API 网关 解决「从哪进、横切怎么做」。

学完你能
  • 对比 Eureka 与 Nacos 选型,并在 ShopFlow 落地 Eureka
  • 让 order-service、inventory-service 注册、续约与下线
  • 用 Spring Cloud Gateway 配置路由、谓词与过滤器
  • 透传鉴权头、重写路径,并用 Docker Compose 起本地最小栈
本章先记住 3 件事
  • 先注册中心、再业务服务、最后网关:实例列表为空时 Gateway 无法路由。
  • 网关只做横切:路由、鉴权头、限流;业务编排仍在 order-service(见 通信模式)。
  • 心跳 ≠ 强一致:注册表是近似视图,调用仍可能命中刚下线的实例(下一章 LoadBalancer + 超时重试)。

Eureka / Nacos 注册中心选型对比

服务注册发现:服务启动时向注册中心登记自己的 host、port、元数据;消费者从注册中心查询可用实例,而不是写死 URL。ShopFlow 本课选用 Netflix Eureka(Spring Cloud 一等公民);Nacos 在阿里生态与配置中心一体化场景更常见。

Eureka vs Nacos(ShopFlow 视角)

维度EurekaNacos
Spring Cloud 集成 eureka-server/client 成熟,本课 BOM 直接管理版本 spring-cloud-alibaba 另需对齐 BOM
配置中心 否(Config Server 另章) 注册 + 配置一体
健康模型 客户端心跳 + 服务端剔除 心跳 + 可选 CP 模式
本课选择 ✓ 教程默认,Compose 一键起 延伸阅读;生产可评估
硬编码地址
  • http://192.168.1.8:8081 写进 Feign
  • 扩容 / 换端口要改代码
  • 无法感知实例宕机
注册发现
  • 按 spring.application.name 查实例
  • LoadBalancer 轮询 / 随机
  • 心跳过期后剔除(最终一致)

order-service、inventory-service 注册与心跳

在 脚手架 基础上,为各 service 模块添加 Eureka Client(版本由 Spring Cloud BOM 管理),并新增独立模块 shopflow-discovery-server(或 Docker 镜像)作为注册中心。

Discovery Server(Eureka Server)

java
@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServerApplication {
  public static void main(String[] args) {
    SpringApplication.run(DiscoveryServerApplication.class, args);
  }
}
yaml
# discovery-server / application-local.yml
server:
  port: 8761
eureka:
  client:
    register-with-eureka: false
    fetch-registry: false

业务服务注册(Client)

xml
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
yaml
# order-service / application-local.yml
spring:
  application:
    name: order-service
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true
    lease-renewal-interval-in-seconds: 10

inventory-service 同理,spring.application.name 必须为 inventory-service(与 Gateway 路由 lb://inventory-service 一致)。

ShopFlow · 注册与心跳
sequenceDiagram
  participant O as order-service
  participant E as Eureka :8761
  participant I as inventory-service
  O->>E: REGISTER (8081)
  I->>E: REGISTER (8082)
  loop 每 10s
    O->>E: RENEW 心跳
    I->>E: RENEW 心跳
  end
  Note over E: 超时未续约则剔除实例

注册表是软状态;调用方仍可能遇到 部分失败,需客户端超时与重试。

Spring Cloud Gateway 路由与谓词过滤器

新增 shopflow-gateway 模块,依赖 spring-cloud-starter-gateway-server-webflux(Spring Cloud 2025 命名;WebFlux 网关为 ShopFlow 默认)。Gateway 从 Eureka 拉取实例,按路由(Route) = 谓词(Predicate)+ 过滤器(Filter)+ 目标 URI 转发流量。

最小路由:按路径前缀

yaml
# gateway / application-local.yml(Spring Cloud 2025 属性前缀)
server:
  port: 8080
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      server:
        webflux:
          routes:
            - id: order-route
              uri: lb://order-service
              predicates:
                - Path=/api/orders/**
            - id: inventory-route
              uri: lb://inventory-service
              predicates:
                - Path=/api/inventory/**

lb:// 表示通过 Spring Cloud LoadBalancer 从 Eureka 选实例。若从旧版迁移,可用 spring-boot-properties-migrator 对照 spring.cloud.gateway.* → spring.cloud.gateway.server.webflux.*。

  • Path:路径匹配,最常用
  • Header、Method:按头或 HTTP 方法
  • After / Before:时间窗口(金丝雀可扩展)
  • AddRequestHeader:注入 traceId、租户 ID
  • StripPrefix:去掉路径前缀再转发
  • RequestRateLimiter:限流(advanced 章展开)

统一入口:鉴权头透传与路径重写

外部客户端只访问 https://api.shopflow.example(本地 :8080),Gateway 负责 TLS 终止(生产)、JWT 校验(安全与部署 章)与把用户身份传给下游。业务服务不应再对外暴露端口。

StripPrefix + 鉴权头透传

yaml
spring:
  cloud:
    gateway:
      server:
        webflux:
          routes:
            - id: order-api
              uri: lb://order-service
              predicates:
                - Path=/api/orders/**
              filters:
                - StripPrefix=1
                - AddRequestHeader=X-ShopFlow-User-Id, #{headers['X-ShopFlow-User-Id']}
# 外部: GET /api/orders/123  →  order-service: GET /orders/123

本章仅演示头透传占位;完整 OAuth2 Resource Server 在 Gateway 统一验签后,将 sub 写入 X-ShopFlow-User-Id 即可。勿在 Gateway 写「下单编排」——那是 order-service 职责。

先想 10 秒:为何 StripPrefix=1 后,下游 Controller 仍映射 /orders/{id}?

对外 API 带 /api 命名空间便于版本与 BFF 区分;对内 微服务 保持领域路径简洁。StripPrefix 去掉第一段 api,避免每个 Controller 重复写前缀。

本地 Docker Compose 最小依赖栈

本地联调推荐 Compose 起 Eureka;业务服务仍可用 IDE / Maven 启动便于断点。Gateway 与 order / inventory 需能访问 8761。

compose 片段

yaml
# demos/practice-02-discovery-gateway-lab/docker-compose.yml
services:
  discovery-server:
    image: eclipse-temurin:21-jre
    ports:
      - "8761:8761"
    # 实际项目: 挂载 discovery-server.jar 或 build 镜像
    environment:
      SPRING_PROFILES_ACTIVE: docker

推荐启动顺序

  1. docker compose up -d discovery-server(或本地跑 DiscoveryServerApplication)
  2. 打开 http://localhost:8761,确认 Eureka 控制台可用
  3. 启动 inventory-service(8082)、order-service(8081),在控制台看到 UP 实例
  4. 启动 api-gateway(8080),curl http://localhost:8080/api/orders/actuator/health(经路由转发)
验收要点
  • Eureka 上至少 2 个应用:ORDER-SERVICE、INVENTORY-SERVICE(大写为默认展示)
  • 经 Gateway 访问成功,直连业务端口仅用于调试
  • 停掉 inventory 后,Gateway 路由应在一段时间后不再转发到该实例

本章结论

每条对应上一节 h3。

  • 注册中心选型:ShopFlow 用 Eureka + Spring Cloud BOM;Nacos 适合需配置一体的阿里栈,另评估 BOM。
  • 注册与心跳:eureka-client + defaultZone;spring.application.name 即服务 ID;心跳续约后注册表才可靠。
  • Gateway 路由:Path 谓词 + lb://service-id;2025 属性前缀 gateway.server.webflux。
  • 统一入口:StripPrefix 与鉴权头透传;网关不做业务编排。
  • 本地栈:Compose 起 Eureka → 业务服务 → Gateway;按顺序验收控制台与 curl。

复习与自检

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

0 / 5 已勾选

下一步:完成 Demo 与测验;继续 OpenFeign、负载均衡与服务间调用,实现 order 同步调用 inventory 预留库存。

动手练习

按「推荐启动顺序」搭 Eureka + order/inventory + Gateway,完成 8080 转发与实例剔除观察。

操作步骤

  1. 启动 discovery-server(8761),对照 register-with-eureka: false 解释为何 Server 不注册自己。
  2. 为 order/inventory 添加「业务服务注册」yaml,确认控制台 ORDER-SERVICE、INVENTORY-SERVICE 为 UP。
  3. 写入 Gateway 路由 yaml(Path=/api/orders/** → lb://order-service),curl http://localhost:8080/api/orders/actuator/health 经网关可达。
  4. 增加 StripPrefix=1,说明 /api/orders/... 到下游 /orders/... 的映射。

判断练习

  1. Gateway 先于 Eureka 启动最可能导致长时间 503。请写出正确启动顺序(① Eureka ② inventory ③ order ④ Gateway)。

    参考答案

    正确顺序:① → ③ → ② → ④(或 ① → ② → ③ → ④,业务服务顺序可互换,但必须在 Gateway 前)。Gateway 依赖注册表中有可用实例,否则 lb:// 无法解析。

  2. 路由写 uri: http://localhost:8081 而非 lb://order-service,且已有 2 实例。会有什么问题?改哪一行?

    参考答案

    无法负载均衡与故障转移,扩缩容需改 Gateway 配置。应改路由 uri 为 lb://order-service,由 LoadBalancer 从 Eureka 选实例。

Demo:Eureka + Gateway 本地栈

目录:demos/practice-02-discovery-gateway-lab/。验收:Eureka 2 应用 UP + 8080 curl health 成功 + 停 inventory 后观察剔除。

章节测验

学完「分布式系统本质与挑战」后作答。热身+理解+场景可先「检查」再看「提示」或「答案」。

0. (热身·单选)ShopFlow 三大限界上下文不包括哪一个?

1. (单选)「部分失败」最典型的特征是?

2. (填空)ShopFlow 中,负责「预留/扣减库存、防超卖」的限界上下文是 ______。

3. (多选)下列哪些属于「从单机走向分布式」的常见驱动力?

4. (单选)关于跨节点「时钟漂移」,下列做法错误的是?

5. (场景·单选)order-service 调用 inventory-service 超时,但库存侧可能已成功扣减。较稳妥的首选策略是?

章节测验

学完「CAP、BASE 与一致性模型」后作答。理解+场景可先「检查」再看「提示」或「答案」。

1. (单选)发生网络分区时,CAP 定理要求系统如何取舍?

2. (填空)BASE 中的 E 指 ______ consistent(最终一致)。

3. (多选)下列哪些属于本章讨论的一致性模型?

4. (单选)Fencing token 的主要作用是?

5. (单选)用户刚下单后刷新「我的订单」却看不到刚创建的订单,最可能缺少哪种客户端语义?

章节测验

学完「架构风格:单体、SOA 与微服务」后作答。理解+场景+判断可先「检查」再看「提示」或「答案」。

1. (单选)模块化单体与「分层大泥球单体」的主要区别是?

2. (填空)SOA 时代常把路由、转换与编排集中在 ______(三字母缩写)上,易成变更瓶颈。

3. (多选)下列哪些属于微服务带来的典型成本(而非收益)?

4. (单选)关于康威定律,下列说法最准确的是?

5. (场景·单选)ShopFlow 仅 6 人,领域边界每周调整,尚无 CI/CD 与集中追踪。较稳妥的首选是?

章节测验

学完「领域建模、边界上下文与 API 契约」后作答。理解+场景+判断可先「检查」再看「提示」或「答案」。

1. (单选)关于限界上下文,下列说法最准确的是?

2. (填空)契约优先实践中,团队应先评审 ______ 规范再实现 Controller(常用 YAML 格式)。

3. (多选)下列哪些属于 REST API 设计的推荐做法?

4. (单选)消费者驱动契约(CDC) 的核心思想是?

5. (场景·单选)payment-service 收到渠道回调后,直接用 JDBC 更新 order 库的 status。较符合本章原则的做法是?

章节测验

学完「通信模式:同步、异步与事件驱动」后作答;basics 阶段收官。理解+场景+判断

1. (单选)ShopFlow 中 order → inventory 的「预留库存」较适合哪种通信?

2. (填空)「写业务表 + 发消息」原子性常用 Transactional ______ 模式(与业务同事务写待发事件表)。

3. (多选)下列哪些属于异步事件驱动的典型收益?

4. (单选)关于编舞(Choreography),下列说法最准确的是?

5. (场景·单选)order 同步调用 payment 并阻塞 30s 等待渠道回调。较符合本章推荐的做法是?

章节测验

学完「Spring Boot 3.5 与 Spring Cloud 脚手架」后作答。热身+理解+场景可先「检查」再看「提示」或「答案」。

0. (热身·单选)ShopFlow 父 POM 中,Spring Cloud 组件版本应由谁统一管理?

1. (单选)shopflow-order-api 模块最适合放置哪类代码?

2. (填空)本地开发时,通过 spring.profiles.active=______ 激活 application-local.yml(填写 Profile 名,英文)。

3. (多选)关于 Actuator /actuator/health,下列哪些说法正确?

4. (场景·单选)团队想在 order-service 的 pom 里写死 spring-cloud-starter-openfeign 版本 4.0.0,而父 POM Cloud BOM 为 2025.0.1。较符合本章规范的做法是?

5. (判断·单选)本章本地启动 order-service 前,必须先启动 Eureka 注册中心,否则 health 无法 UP。

章节测验

学完「服务注册发现与 API 网关」后作答。热身+理解+场景可先「检查」再看「提示」或「答案」。

0. (热身·单选)ShopFlow 本课默认选用哪类注册中心?

1. (单选)Gateway 路由中 uri: lb://order-service 的 lb:// 表示?

2. (填空)ShopFlow 本地 Eureka 控制台默认端口为 ______(数字)。

3. (多选)下列哪些适合放在 API Gateway 层?

4. (场景·单选)本地启动顺序错误:先起 Gateway,再起 Eureka,最后起 order-service。最可能出现?

5. (判断·单选)Eureka 心跳续约成功后,调用方就永远不会请求到已宕机实例。

豆包 ↗ DeepSeek ↗ 通义 ↗