阶段一 · 分布式基石
CAP/BASE、架构风格选型、DDD 边界与 API 契约、同步/异步通信模式;为 ShopFlow 拆分打底。
从 CAP、领域建模与通信模式,到 Spring Cloud 微服务实战(ShopFlow 示例),再到韧性、可观测与架构演进——按三阶段系统掌握 Java 分布式架构设计与落地。
| 阶段 | 章节 | 小节概要 |
|---|
CAP/BASE、架构风格选型、DDD 边界与 API 契约、同步/异步通信模式;为 ShopFlow 拆分打底。
Spring Cloud 脚手架、注册发现与网关、OpenFeign 调用、Kafka/Redis、分布式事务与 Saga mini 项目。
熔断限流、可观测性、安全与 K8s 部署、容量扩展、CQRS/反模式与架构评审。
请从左侧目录选择章节;建议从「分布式系统本质与挑战」读起。打开过的章节侧栏显示圆点(进行中),测验与过关清单完成后可点章首标记完成(✓)。进度与清单勾选可通过顶栏导出/导入。
这是全课起点。若把远程调用当本地方法、把超时当「偶发 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 超时」时序,并填好四格表中「超时」行。
分布式系统本质与挑战 讲了「部分失败」;本章回答:分区发生时在 C 与 A 之间选什么、为何常见 AP + 最终一致,以及「读己之写」从哪来。
POST /reserve、商品详情、支付回调各说一句分区时的 CP/AP 取向CAP 定理(Brewer)指出:在存在网络分区时,分布式系统无法同时满足:
真实部署几乎必须接受 P,于是分区发生时要在 C 与 A 间做选择——不是「三选二」的日常开关,而是按业务路径分别设计。
flowchart TB
subgraph normal [无分区]
W[写入成功] --> R[读到最新]
end
subgraph partition [发生分区 P]
CP[选 CP] -->|拒绝部分读写| ERR[返回错误 / 降级]
AP[选 AP] -->|继续服务| STALE[可能读到旧副本]
end
normal --> partition
CP:宁可失败也不返回不确定的旧数据;AP:宁可旧数据也要保持入口可用。
无分区时仍要在延迟(L)与一致(C)间权衡:例如跨地域复制常降低一致级别以换更低延迟。设计评审时可问:「正常时要多一致?分区时要多可用?」
多数选 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 章节展开) |
BASE 是对 AP 路径的实操总结(与 ACID 相对):
| 维度 | ACID(单库事务) | BASE(跨服务协作) |
|---|---|---|
| 边界 | 一个数据库连接内 | 多服务、多库、消息 |
| 失败 | 回滚 | 补偿、重试、对账 |
| ShopFlow | 单服务内订单行 + 明细 | 下单 → 库存 → 支付链(详见 分布式数据:事务、幂等与 Saga) |
「写订单表 + 发 Kafka」无法靠本地事务一起提交。常见做法:Outbox 与业务写入同一事务,再由投递器异步发消息。
@Transactional
public void createOrder(CreateOrder cmd) {
orderRepository.save(cmd.toOrder());
outboxRepository.save(OutboxEvent.orderCreated(cmd.orderId()));
// 同一 DB 事务;消息由 poller 异步投递,消费者须幂等
}
跨节点互斥(定时任务抢主、库存扣减串行化)常用分布式锁。实现须考虑:锁持有者崩溃、时钟漂移、旧锁未释放就获得新锁。
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 的写。
// 下游无法区分「旧主迟到的写」
String lockVal = UUID.randomUUID().toString();
redis.set("lock:job", lockVal, NX, PX, 30000);
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) | 改头像后评论里头像一起更新 | 用户资料变更广播或短期读主 |
shopflow:
datasource:
read-your-writes: true
# 创建订单后的 5s 内,该 userId 查询走主库
下一章讨论 架构风格:单体、SOA 与微服务,把本章取舍落到架构形态选择。
本章回答「分区与复制时数据有多新」。下面几条是验收口径,可对照各节与 ShopFlow 接口回忆。
POST /reserve 防超卖宜强一致或串行化,商品详情等读多场景可接受短暂旧读。过关清单(勾选会保存在本浏览器)
0 / 5 已勾选
| 概念 | 一句 |
|---|---|
| CAP | 有 P 时分区要在 C/A 间选 |
| 最终一致 | 停止写后会收敛,运行中可能旧读 |
| BASE | 可用 + 软状态 + 最终一致 + 补偿 |
| Fencing | 单调 token,存储拒绝旧写 |
| 读己之写 | 用户看到自己刚提交的数据 |
下一步:勾选过关清单、完成测验与 Demo;下一章 架构风格:单体、SOA 与微服务。
为 ShopFlow 三个接口标注 CAP 取向,并完成fencing 时序与读己之写两处纸面标注(约 25 分钟)。
POST /reserve、GET /products/{id}、支付回调各写一句:分区时偏 CP 还是 AP,并引用正文 CAP 流程图一句理由。大促商品详情可略旧库存,但 POST /reserve 须防超卖。为何前者可偏 AP、后者偏 CP?各写一句。
商品详情读多写少、短暂旧值可接受,可用 AP + 最终一致。预留库存须当场防超卖,分区时更倾向 CP/串行化或强一致路径,否则会出现双卖。
库存扣减用 Redis 锁,锁值为固定 UUID、无 fencing token。主要违反哪一节?说明「僵尸节点」风险。
违反「分布式锁、租约与 fencing token」一节。租约过期后旧主仍可能持锁并写库;无递增 fencing token 时,存储无法拒绝更旧的写,新主数据可能被覆盖。
目录:demos/basics-02-consistency-cap-lab/。验收:接口卡片 3 张均已标注 CP/AP + 一句理由。
分布式系统本质与挑战 与 CAP、BASE 与一致性模型 讲了分布式问题与数据取舍;本章回答:要不要拆、拆成什么形态。盲目 微服务 会把 CAP、BASE 与一致性模型 中的 CAP、部分失败 成本成倍放大。
分层单体(Layered Monolith):按技术层划分包结构,所有业务能力共享同一部署单元与数据库。优点是上手快、本地调试简单、事务在进程内完成;风险是边界模糊时演变成「大泥球」——改订单逻辑可能误触库存表。
flowchart TB UI[Web / API Controller] --> APP[Application Service] APP --> DOM[Domain Model] DOM --> INF[Infrastructure / JPA] INF --> DB[(单一数据库)]
所有业务共享一个 DB 与一次发布;适合团队小、领域尚在高速试错阶段。
仍在一个进程、一次部署,但按业务能力或限界上下文划分子模块(Maven/Gradle 多模块),模块间通过显式 API 通信,禁止随意跨模块访问对方表。
shopflow-order、shopflow-inventory、shopflow-payment 模块,共享 shopflow-app 启动器。// 反例:订单服务直接操作库存表
@Service
class OrderService {
@Autowired InventoryRepository inventoryRepo; // 跨上下文泄漏
void placeOrder(...) {
inventoryRepo.decrement(skuId, qty);
}
}
// 推荐:通过 inventory 模块公开门面
@Service
class OrderService {
private final InventoryFacade inventory;
void placeOrder(...) {
inventory.reserve(new ReserveCommand(orderId, skuId, qty));
}
}
2000 年代前后,面向服务架构(SOA) 强调将企业能力封装为可复用服务,常配合企业服务总线(ESB) 做路由、转换、编排与集中治理。
timeline title 从集中到分布式的常见路径 2000s : 分层单体 / 大泥球 2005-2015 : SOA + ESB 集中编排 2014+ : 微服务 + 轻量消息总线 / API 网关 近年 : 模块化单体复兴 + 选择性微服务
不是「微服务取代一切」,而是按组织成熟度在谱系上移动。
flowchart LR O[订单系统] --> ESB[ESB 总线] I[库存系统] --> ESB P[支付系统] --> ESB ESB --> T[转换 / 路由 / 编排规则]
所有集成逻辑汇聚到总线:强大但易成瓶颈与「分布式大泥球」的控制中心。
微服务不是「更先进的单体」,而是用运维与组织复杂度交换独立演进能力。下表帮助你在评审会上说清利弊。
| 维度 | 收益 | 成本 / 风险 |
|---|---|---|
| 交付 | 团队按服务独立发布,减少「全站回归 2 周」 | 集成测试、契约测试、环境矩阵变多 |
| 技术 | 可按服务选型(如搜索用 Elasticsearch) | 统一可观测、安全基线、依赖治理难度上升 |
| 弹性 | 故障隔离(inventory 挂了不必拖垮整站) | 分布式事务、Saga、幂等、链路追踪必须到位 |
| 组织 | 与 康威定律 对齐,责任清晰 | 需要平台工程、SRE、API 治理投入 |
| 信号 | 更倾向 | 说明 |
|---|---|---|
| 团队 < 10 人,领域边界每周变 | 模块化单体 | 先固化边界,再付分布式税 |
| 3+ 团队并行,发布互相阻塞 | 微服务(边界已清) | 独立部署回报大于集成成本 |
| 强一致跨库事务是常态 | 暂缓拆分或保留单体事务 | 拆后须 Saga / 2PC,复杂度高 |
| 读多写少、可接受最终一致 | 微服务 + 事件 | 与 CAP、BASE 与一致性模型 AP 路径一致 |
当 Order / Inventory / Payment 由不同小队维护、且库存接口变更每周阻塞订单发布时,拆成三个可部署服务才有明显回报;否则模块化单体 + 清晰模块 API 往往足够。
康威定律(Conway's Law):设计系统的组织,其产生的设计等价于组织内的沟通结构。换言之一一你的架构图最终会像你的组织架构图。
payment-service 作为独立部署单元更自然。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 实体互相引用,无法一句话说清谁拥有哪张表。通常否:优先模块化单体 + 清晰门面;当发布阻塞或团队分组成熟后再拆。拆早会把 CAP、BASE 与一致性模型 中的 CAP、部分失败成本全部兑现且无人力消化。
下一章进入 领域建模、边界上下文与 API 契约,把「模块/服务边界」落到限界上下文与 OpenAPI。
每条对应上一节 h3,用于架构评审前的 30 秒复述。
过关清单(勾选会保存在本浏览器)
0 / 5 已勾选
| 概念 | 一句 |
|---|---|
| 模块化单体 | 单部署,模块间只走公开 API |
| ESB | 集中编排,易成瓶颈 |
| 微服务 | 独立部署 + 自有数据 + 组织对齐 |
| 康威定律 | 架构镜像沟通结构 |
| 不宜拆 | 边界糊、无自动化、团队过小 |
下一步:勾选过关清单、完成测验与 Demo;下一章 领域建模、边界上下文与 API 契约 固化限界上下文与 API 契约。
用「架构形态决策表」与康威定律,为 ShopFlow(7 人 / 日单 200)写架构选型备忘。
「为用上 K8s,先把 Order 拆成 5 个微服务,团队仍是一个 7 人全栈小队。」违反哪 2 条原则?更稳妥的第一步?
违反康威定律(组织未拆、服务先拆)与不宜硬拆信号(边界/自动化不足)。第一步:模块化单体 + ArchUnit 约束。
哪条更像 SOA+ESB 反模式?① Gateway 只路由鉴权 ② 所有 JSON↔XML 转换集中在 ESB ③ 各服务维护 OpenAPI
答:②。ESB 成为转换与编排瓶颈,变更需总线团队审批;①③ 符合智能端点 + 契约分散维护。
目录:demos/basics-03-architecture-styles-lab/。验收:团队拓扑表三行与决策表「约束」列已填完。
架构风格:单体、SOA 与微服务 选了「单体还是微服务」;本章回答边界画在哪、对外说什么语言。没有清晰的 限界上下文 与 OpenAPI 契约,拆服务只会得到多个互相渗透的「小泥球」——集成靠口头约定,改接口即全站停摆。
领域驱动设计(DDD)战略设计关注「大问题如何切」:识别子域(Subdomain)类型,并为每个核心能力划定限界上下文(Bounded Context)。
| 类型 | 含义 | ShopFlow 示例 |
|---|---|---|
| 核心域 | 差异化竞争力,值得自研投入 | 订单编排、库存防超卖策略 |
| 支撑域 | 必需但非差异化,可适度定制 | 支付对接、物流通知 |
| 通用域 | 行业通用能力,优先买/用 SaaS | 短信验证码、对象存储 |
OrderLine 与 Inventory 里的 StockReservation 不必强行合并。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 或事件),不是共享数据库。各上下文内部模型互不直接引用。
Warehouse 字段skuId、qty 等集成所需最小字段GET /skus/{id} 按需查询架构风格:单体、SOA 与微服务 已对比分层单体与模块化单体;本节从 DDD 视角强调:微服务边界应沿业务能力(限界上下文)切,而非沿 Controller / Service / Repository 等技术层切。
| 维度 | 按技术层拆分 | 按业务能力 / 限界上下文拆分 |
|---|---|---|
| 包/服务结构 | user-controller、order-controller 各一服务,共享同一 DB |
order-service、inventory-service,各管各的数据 |
| 变更影响 | 改订单逻辑仍可能动「公共 order 表」 | 订单模型变更局限在 Order 上下文内 |
| 团队对齐 | 按「前端组 / 后端组」分工,与业务脱节 | 订单小队拥有 Order 上下文全栈 |
| 分布式代价 | 背上网络调用却无独立数据边界 → 分布式单体 | 真正独立部署与演进,但须设计 API/事件与一致性 |
// 反例:按「层」拆服务,却共享 order 表与实体
// order-query-service 直接读 order 表 join inventory 表
@Query("SELECT o, i FROM Order o JOIN Inventory i ON ...")
List<OrderDetailDto> findAllWithStock();
// 推荐: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);拆分维度是业务能力,不是禁止分层。
限界上下文对外通常以 RESTful HTTP API 暴露能力(practice 阶段亦用 OpenFeign 消费)。好的资源建模让调用方「读 URI 即懂语义」,并预留演进空间。
/orders、/skus/{skuId}/reservations,避免 /createOrder 动词路径。POST /orders/{id}/cancel 或 PATCH /orders/{id} + {"status":"CANCELLED"}。skuId,不在 URI 里嵌 inventory 内部仓库 ID,除非该 ID 已是稳定集成标识。POST /reservations)要求 Header Idempotency-Key 或 body 内 requestId,应对 部分失败 重试。| 上下文 | 方法 | 路径 | 说明 |
|---|---|---|---|
| Order | POST | /api/v1/orders | 创建订单 |
| Order | GET | /api/v1/orders/{orderId} | 查询订单 |
| Inventory | POST | /api/v1/reservations | 预留库存(幂等) |
| Inventory | GET | /api/v1/skus/{skuId} | 查询 SKU 可售量 |
| Payment | POST | /api/v1/payments | 发起支付 |
/api/v1/orders → /api/v2/orders:直观,网关路由简单/api/v1Accept: application/vnd.shopflow.order.v2+jsonApi-Version: 2{
"code": "INVENTORY_INSUFFICIENT",
"message": "SKU-1001 可售库存不足",
"traceId": "7f3a2b1c-...",
"details": { "skuId": "SKU-1001", "requested": 5, "available": 2 }
}
ORDER_ / INV_,网关可映射为统一表)。code 表达业务层语义。契约优先(Contract First):先写 OpenAPI 规范,再生成/实现 Controller 与客户端桩代码。这样 Order 与 Inventory 团队在编码前就对齐字段、错误码与幂等语义。
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: 库存不足或冲突
openapi/inventory-api.yaml。在 微服务 集成中,调用方(消费者) 定义「我需要哪些字段与状态码」,写入契约文件;提供方 在 CI 中验证能否满足。这样 inventory 重构内部表结构时,只要仍满足 order-service 的契约,集成测试即可通过。
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 阶段。
团队应显式选定真源:契约优先则以 YAML 为准;代码优先则须 CI 反向生成/校验 OpenAPI diff。ShopFlow 教程采用契约优先,与 SpringDoc 可并存(由 YAML 生成或双向校验)。
将本章方法落到 ShopFlow 全课主线:三个上下文各司其职,通过 API 网关 对外统一入口,内部走同步 API + 异步事件。
| 上下文 | 核心聚合 | 拥有的数据 | 对外能力 |
|---|---|---|---|
| Order | Order | 订单头、明细、状态机 | 创建/查询/取消订单;订阅 PaymentCompleted |
| Inventory | Sku、Reservation | 可售量、预留记录 | 预留/释放/扣减;强一致 防超卖 |
| Payment | Payment | 支付单、渠道回调 | 发起支付、退款;发布 PaymentCompleted |
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)展开。
InventoryClient 将外部 DTO 转为域内 ReservationResult,隔离外部模型变化。不应:违反上下文数据所有权。应通过 API 或事件让 Order 上下文自行变更状态;跨库写是 分布式单体 典型信号。事件可靠投递可结合 Outbox(CAP、BASE 与一致性模型 预览,Kafka 事件驱动与 Redis 缓存 展开)。
下一章 通信模式:同步、异步与事件驱动 将把同步/异步、编排与编舞、Outbox 与本章契约映射到 Spring Cloud 概念。
每条对应上一节 h3,用于评审前 30 秒复述。
过关清单(勾选会保存在本浏览器)
0 / 5 已勾选
| 概念 | 一句 |
|---|---|
| 限界上下文 | 独立语言与模型;对外只暴露 API/事件 |
| 按能力拆分 | Order/Inventory/Payment,非按 Controller 拆 |
| 契约优先 | OpenAPI 先评审再编码 |
| CDC | 消费者定义期望,提供者 CI 验证 |
| 幂等键 | 写操作 Header Idempotency-Key |
下一步:勾选过关清单、完成测验与 Demo;下一章 通信模式:同步、异步与事件驱动 讲同步/异步通信与 Outbox 预览。
产出 Inventory 最小 OpenAPI 与三上下文映射图,对照错误码与幂等头约定自检。
POST /api/v1/reservations 补全 409 库存不足与幂等冲突的 JSON body(含 code、message)。Idempotency-Key header 参数,与正文 OpenAPI 片段对齐。PaymentCompleted 事件方向。哪条 API 设计违反契约?① Inventory 暴露 PATCH /orders/{id}/status ② Order 调 POST /reservations 带幂等键 ③ Payment 回调用 callbackToken 验签
答:①。订单状态属 Order 上下文,Inventory 不应直接改 orders;应通过 API/事件让 Order 自行变更。②③ 符合契约与幂等/验签约定。
OpenAPI 缺少 Idempotency-Key header,却写「支持重复提交安全」。违反哪条约定?应改哪一段?
违反契约优先与预留接口幂等约定。应在 OpenAPI parameters 增加 header 定义,并在 POST 说明中引用;实现与文档保持一致。
目录:demos/basics-04-domain-api-lab/。验收:YAML 含 reservations 路径 + 响应码 201/409/422。
前四章定了边界与契约;本章回答服务之间怎么说话。选错模式会把 部分失败 放大成长链路阻塞,或把本该异步的通知硬拉成同步耦合。
请求/响应(Request/Response)是分布式中最直观的模式:调用方发请求、阻塞或异步等待响应。在 Java 微服务中常体现为 REST + OpenAPI 契约,practice 阶段用 OpenFeign 消费。
| 风格 | 特点 | ShopFlow 示例 |
|---|---|---|
| REST/HTTP + JSON | 通用、易调试、网关友好;性能略低于二进制 RPC | order → inventory 预留、查询 SKU |
| RPC(如 gRPC) | 强类型契约、HTTP/2、流式;需生成桩代码 | 高 QPS 内部调用、多语言团队(可选) |
| GraphQL / BFF 聚合 | 前端一次取多资源;聚合层负责扇出 | 商品详情页聚合(性能、容量与数据扩展 再展开) |
# 详见 OpenFeign、负载均衡与服务间调用;此处仅示意超时与幂等头
spring:
cloud:
openfeign:
client:
config:
inventory-service:
connectTimeout: 1000
readTimeout: 2000
# 调用时 Header: Idempotency-Key: {orderId}
当调用方不必立即知道下游处理结果,或需要削峰、解耦多订阅者时,使用异步消息。ShopFlow 中 PaymentCompleted、OrderCreated 等领域事件适合走 Kafka 等 MQ(详见 Kafka 事件驱动与 Redis 缓存)。
OrderCreated、PaymentCompleted),载荷含最小集成字段 + 事件 ID。flowchart LR Pay[payment-service] -->|publish| K[(Kafka topic)] K --> G1[order-service 消费者组] K --> G2[analytics 消费者组] G1 --> O[更新订单 PAID] G2 --> A[报表入库]
order 与 analytics 各用消费者组,互不影响进度;须幂等处理重复投递。
跨多个服务的业务流程(如下单 → 预留 → 支付 → 确认)如何协调?两种主流思路:编排(Orchestration) 由中央协调者按脚本推进;编舞(Choreography) 各服务监听事件、自行决定下一步,无中心大脑。
| 维度 | 编排 | 编舞 |
|---|---|---|
| 协调者 | order-service 或独立 orchestrator 调用各步 | 各服务订阅事件,本地状态机驱动 |
| 可见性 | 流程集中在一点,易画序列图 | 流程分散,须链路追踪串联 |
| 耦合 | 协调者知悉所有步骤 API | 服务只关心相关事件,添加订阅者较易 |
| 失败处理 | 常配合 Saga 补偿,由协调者触发 | 每服务自行补偿或发补偿事件 |
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 网关 里写编排逻辑。
异步与重试使「至少一次投递」成为常态。须从生产端与消费端双侧保证正确性。
Idempotency-Key;异步用 eventId 或业务键。processed_events(event_id),重复则跳过。问题:订单入库与发 Kafka 两个操作无法共用一个本地事务。 Outbox 将待发事件与业务写入同一数据库事务,后台投递器异步发到 MQ。
@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
}
第二次应识别为重复:查去重表或订单状态已为 PAID,直接 ack 不再改库。若仍重复扣减或重复发券,说明缺少消费端幂等。
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 脚手架 |
你已具备分布式问题域、CAP、架构形态、DDD 边界、API 契约与通信选型基础。下一阶段在 ShopFlow 仓库中动手搭服务,建议从 Spring Boot 3.5 与 Spring Cloud 脚手架 开始。
每条对应上一节 h3,basics 阶段收官复述。
过关清单(勾选会保存在本浏览器)
0 / 5 已勾选
| 概念 | 一句 |
|---|---|
| 同步 | 当场要结果;宜短链 + 幂等 |
| 异步 | 事件解耦;消费幂等 |
| 编排 | 中央协调多步 |
| 编舞 | 各服务听事件自行反应 |
| Outbox | 写库与待发事件同事务 |
下一步:勾选过关清单、完成测验与 Demo;进入 practice 阶段,从 Spring Boot 3.5 与 Spring Cloud 脚手架 搭建 ShopFlow 工程。
填写同步/异步选型与 Spring Cloud 映射表,并解释 Outbox 与 createOrder 代码块关系。
createOrder + OutboxEvent 片段:去掉 outbox 直接发 Kafka 会破坏哪条 BASE 配套手段?「order→inventory→payment→短信→积分」全 OpenFeign 同步且无超时。最先应改哪一处?
支付改异步事件(或至少缩短链并设超时)。支付结果不必阻塞 order 线程;全同步长链最易触发「同步滥用」中的线程堆积与级联失败。
PaymentCompleted 被 order 与 analytics 分别消费——编舞还是编排?新增 analytics 订阅者应改 order 还是只加消费者?
analytics 侧属编舞(听事件自行处理)。新增 analytics 订阅者只加 Kafka 消费者,无需改 order 编排代码,这是编舞扩展性优势。
目录:demos/basics-05-communication-patterns-lab/。验收:集成点选型表 3 行均已填模式 + 理由。
basics 阶段定了边界与通信;practice 从可运行的工程骨架开始。版本对齐(Boot + Cloud BOM)、模块分层与 Profile 约定若在一开始做错,后续注册发现、Feign、Kafka 会在依赖冲突与配置漂移上反复踩坑。
application.yml、Profile 与本地启动顺序spring-cloud-dependencies 统一管理,子模块勿手写版本号。本课栈: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 继承版本。
<!-- 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>
openfeign 版本spring-cloud.version<version>.mvn/jvm.config 或 CI 固定 JDK 21可以跑单个 REST 服务,但 ShopFlow 实战依赖注册发现、Gateway、OpenFeign。本章仍引入 Cloud BOM,以便下一章直接加 spring-cloud-starter-netflix-eureka-client 而无需改版本矩阵。
将 领域建模、边界上下文与 API 契约 中的 ShopFlow 三上下文落到 Maven 模块:每个上下文至少 api + service,共享能力放入 common。这与 模块化单体 的分模块思路一致,只是 service 模块将独立进程部署为 微服务。
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 |
<!-- 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>
多服务本地联调时,端口、数据源、日志级别必须可切换且可预测。ShopFlow 约定:
application.yml 放共享默认(应用名、Actuator 暴露端点)。application-local.yml 本地 H2/端口;application-docker.yml 供 Compose(见 practice-02)。spring.profiles.active=local(IDE Run Configuration 或环境变量)。.env / 环境变量占位(生产走 Config Server,见 advanced 章)。spring:
application:
name: order-service
management:
endpoints:
web:
exposure:
include: health,info
endpoint:
health:
show-details: when_authorized
server:
port: 8081
logging:
level:
com.shopflow: DEBUG
同一 artifact 要在本地、CI、容器中使用不同端口与环境;外部化后只需改 Profile 或环境变量,避免重新编译。也与 通信模式 中「契约稳定、部署可变」一致。
推荐 monorepo 根目录 shopflow/,与课程 Demo 对齐。首章只需能启动 order-service 并访问健康检查;inventory / payment 可先留空模块占位。
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/
java -version 为 21;根目录 mvn -q -DskipTests install 编译全模块。shopflow-order-service,mvn spring-boot:run -Dspring-boot.run.profiles=local。http://localhost:8081/actuator/health,期望 {"status":"UP"}。OrderServiceApplication,VM options 加 -Dspring.profiles.active=local。practice-02 将先启动注册中心,再启动 order / inventory 并注册;本章无需 Eureka,但请保持应用名 spring.application.name 与模块名一致,便于后续服务发现。
Spring Boot Actuator 提供生产就绪端点。本地与 K8s 探针都依赖 /actuator/health;统一 JSON 日志则便于后续接入 ELK 与 traceId(可观测性 章展开)。
env、beans 等敏感端点;生产用 Spring Security 或网络策略。<!-- 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/health | 存活/就绪(K8s 探针) |
/actuator/info | 构建版本、Git 信息(可选) |
/actuator/metrics | Micrometer 指标(advanced 章) |
每条对应上一节 h3,practice 阶段起步复述。
spring-cloud-dependencies 2025.0.x BOM;JDK 21;子模块不写 Cloud 组件版本。application.yml + Profile(local/docker);端口与敏感项外部化。spring-boot:run → 验证 /actuator/health。过关清单(勾选会保存在本浏览器)
0 / 5 已勾选
下一步:完成 Demo 与测验;继续 服务注册发现与 API 网关,把 order / inventory 注册到 Eureka 并经由 Gateway 统一入口。
在本地跑通 order-service,完成父 POM、多模块、Profile 与 Actuator 命令级验收。
shopflow/pom.xml 含 spring-cloud.version 与 BOM import;在子模块手写 <version> 观察冲突后改回。application-local.yml 端口,mvn spring-boot:run -Dspring-boot.run.profiles=local,curl -s localhost:8081/actuator/health 期望 UP。"service":"order-service"。端口写进 Java 静态字段且无 application-local.yml。两处应改回外部化的位置?违反哪条 Profile 约定?
删除 Java 硬编码;在 application-local.yml 写 server.port,默认 application.yml 放共享项。违反「环境差异外部化、同一 artifact 多环境复用」。
子模块 pom 中 OpenFeign 写死 4.0.0,与 BOM 2025.0.1 并存。应删哪段?保留有何风险?
删除子模块依赖上的 <version>4.0.0</version>,由 BOM 管理。保留可能导致与 Boot 3.5 矩阵不兼容、集成测试通过但生产 classpath 冲突。
目录:demos/practice-01-spring-cloud-bootstrap-lab/。验收:mvn install 成功 + health 为 UP + 改端口后仍可达。
上一章 能单点启动 order-service;多实例、多服务后客户端不能再记 IP:端口。 服务发现 解决「找谁」;API 网关 解决「从哪进、横切怎么做」。
服务注册发现:服务启动时向注册中心登记自己的 host、port、元数据;消费者从注册中心查询可用实例,而不是写死 URL。ShopFlow 本课选用 Netflix Eureka(Spring Cloud 一等公民);Nacos 在阿里生态与配置中心一体化场景更常见。
| 维度 | Eureka | Nacos |
|---|---|---|
| Spring Cloud 集成 | eureka-server/client 成熟,本课 BOM 直接管理版本 |
spring-cloud-alibaba 另需对齐 BOM |
| 配置中心 | 否(Config Server 另章) | 注册 + 配置一体 |
| 健康模型 | 客户端心跳 + 服务端剔除 | 心跳 + 可选 CP 模式 |
| 本课选择 | ✓ 教程默认,Compose 一键起 | 延伸阅读;生产可评估 |
http://192.168.1.8:8081 写进 Feignspring.application.name 查实例在 脚手架 基础上,为各 service 模块添加 Eureka Client(版本由 Spring Cloud BOM 管理),并新增独立模块 shopflow-discovery-server(或 Docker 镜像)作为注册中心。
@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServerApplication {
public static void main(String[] args) {
SpringApplication.run(DiscoveryServerApplication.class, args);
}
}
# discovery-server / application-local.yml
server:
port: 8761
eureka:
client:
register-with-eureka: false
fetch-registry: false
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
# 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 一致)。
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: 超时未续约则剔除实例
注册表是软状态;调用方仍可能遇到 部分失败,需客户端超时与重试。
新增 shopflow-gateway 模块,依赖 spring-cloud-starter-gateway-server-webflux(Spring Cloud 2025 命名;WebFlux 网关为 ShopFlow 默认)。Gateway 从 Eureka 拉取实例,按路由(Route) = 谓词(Predicate)+ 过滤器(Filter)+ 目标 URI 转发流量。
# 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、租户 IDStripPrefix:去掉路径前缀再转发RequestRateLimiter:限流(advanced 章展开)外部客户端只访问 https://api.shopflow.example(本地 :8080),Gateway 负责 TLS 终止(生产)、JWT 校验(安全与部署 章)与把用户身份传给下游。业务服务不应再对外暴露端口。
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 职责。
/orders/{id}?对外 API 带 /api 命名空间便于版本与 BFF 区分;对内 微服务 保持领域路径简洁。StripPrefix 去掉第一段 api,避免每个 Controller 重复写前缀。
本地联调推荐 Compose 起 Eureka;业务服务仍可用 IDE / Maven 启动便于断点。Gateway 与 order / inventory 需能访问 8761。
# 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
docker compose up -d discovery-server(或本地跑 DiscoveryServerApplication)http://localhost:8761,确认 Eureka 控制台可用inventory-service(8082)、order-service(8081),在控制台看到 UP 实例api-gateway(8080),curl http://localhost:8080/api/orders/actuator/health(经路由转发)ORDER-SERVICE、INVENTORY-SERVICE(大写为默认展示)每条对应上一节 h3。
eureka-client + defaultZone;spring.application.name 即服务 ID;心跳续约后注册表才可靠。Path 谓词 + lb://service-id;2025 属性前缀 gateway.server.webflux。过关清单(勾选会保存在本浏览器)
0 / 5 已勾选
下一步:完成 Demo 与测验;继续 OpenFeign、负载均衡与服务间调用,实现 order 同步调用 inventory 预留库存。
按「推荐启动顺序」搭 Eureka + order/inventory + Gateway,完成 8080 转发与实例剔除观察。
register-with-eureka: false 解释为何 Server 不注册自己。ORDER-SERVICE、INVENTORY-SERVICE 为 UP。Path=/api/orders/** → lb://order-service),curl http://localhost:8080/api/orders/actuator/health 经网关可达。StripPrefix=1,说明 /api/orders/... 到下游 /orders/... 的映射。Gateway 先于 Eureka 启动最可能导致长时间 503。请写出正确启动顺序(① Eureka ② inventory ③ order ④ Gateway)。
正确顺序:① → ③ → ② → ④(或 ① → ② → ③ → ④,业务服务顺序可互换,但必须在 Gateway 前)。Gateway 依赖注册表中有可用实例,否则 lb:// 无法解析。
路由写 uri: http://localhost:8081 而非 lb://order-service,且已有 2 实例。会有什么问题?改哪一行?
无法负载均衡与故障转移,扩缩容需改 Gateway 配置。应改路由 uri 为 lb://order-service,由 LoadBalancer 从 Eureka 选实例。
目录: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 心跳续约成功后,调用方就永远不会请求到已宕机实例。