第7章 分布策略
抵抗透明分布诱惑,依据延迟、失败和一致性选择粗粒度远程边界。
学习目标
- 能解释网络延迟、序列化、超时和部分失败为何改变本地接口的设计边界
- 能用 TypeScript DTO 与 Remote Facade 把细粒度领域调用收敛为可观测的远程契约
- 能根据责任归属、调用粒度、失败恢复和部署约束,判断何时保持本地以及何时分布
为什么第7章分布策略值得单独学习
第7章分布策略的核心不是记住某个 RPC 框架的调用方式,而是承认网络不是一条透明的进程内函数边界。订单服务请求结算服务时,延迟、序列化、超时、重复请求和版本不兼容都会成为业务设计的一部分;如果只把本地接口搬到网络上,调用者就会失去对成本和失败的控制。
↡把原本可能在同一进程内完成的职责拆到不同进程、节点或服务,并为网络延迟与部分失败显式设计契约的架构选择。需要同时描述通信成本、责任所有者和恢复路径。本页以 2024 年中文版公开目录限定第7章的范围,并依据 Martin Fowler 的作者图书页和模式目录独立重写;目录只决定要讲什么,案例、实验、判断题和答案均为本课程原创。
先建立直觉:问题、机制与代价
分布对象最容易制造一种错觉:远程对象看起来仍然像本地对象,于是调用者会继续逐字段读取、逐方法修改,却看不到每次往返都可能跨越网络。真正的设计要把一次远程操作看成有成本的消息交换,明确载荷、超时、重试、幂等键和兼容版本。
↡请求已经发出但结果未知,调用者无法确定远端是否完成业务动作的状态;它要求超时、重试和幂等策略共同处理。不能被包装成普通的布尔返回值。对订单与结算跨进程调用,优先比较保持本地的简单性与跨进程部署的独立性,再用调用次数、载荷大小和失败恢复成本证明分布值得承担。
目录单元到教学证据
第7章 分布策略
第7章分布策略要求把“是否拆开”变成可观察的架构决定:先标出状态所有者、通信方向、调用粒度和恢复责任,再用延迟、错误率与重试结果验证决定。只有当这些证据能在订单与结算跨进程调用中复现,分布边界才不是部署图上的装饰。
7.1 分布对象的诱惑
7.1 分布对象的诱惑提醒我们,远程对象不能假装拥有本地对象的低成本引用语义。若客户端逐个读取名称、地址、余额和订单,就会把网络往返隐藏在看似自然的属性访问里;应先统计往返次数,再判断是否需要批量查询或本地快照。
7.2 远程接口和本地接口
7.2 远程接口和本地接口要求契约显式表达消息边界,而不是让远程实现泄漏领域对象的全部方法。远程接口应围绕一次业务意图组织输入和输出,明确 DTO 字段、错误类型、超时语义与版本兼容规则,让调用者能够估算一次操作的成本。
7.3 必须使用分布的情况
7.3 必须使用分布的情况关注组织、合规、独立扩展和故障隔离等真实约束,而不是把微服务数量当成目标。只有当职责必须跨进程、数据必须由不同所有者管理,或独立部署确实带来收益时,才接受网络延迟与部分失败的额外责任。
7.4 关于分布边界
7.4 关于分布边界要求把高频且强一致的协作尽量留在同一边界,把稳定的业务意图和可恢复的状态转换放在远程边界上。边界太细会产生聊天式调用,边界太粗又会形成难以独立演化的巨型接口,选择必须由调用画像和失败恢复证据推动。
7.5 分布接口
7.5 分布接口强调远程契约应当粗粒度、可演化并可观测。接口需要记录请求标识、幂等键、超时和结果状态,返回 DTO 而不是远程领域对象;这样重试、降级和版本迁移才有明确的落点,调用者也不会依赖远端内部结构。
专属代码案例:订单结算的远程外观
把订单结算跨进程调用切成“本地订单摘要 → DTO → Remote Facade → 结算端口 → 可恢复结果”五个观察点。第7章分布策略的设计草案必须写出谁拥有状态、谁作出业务决定、失败怎样传播,以及网络往返、序列化、部分失败、边界粒度和幂等键中哪个指标最先提示当前方案不再适用。
设计记录采用五个可换行字段:单元键为 poeaa24-chapter-07-distribution-strategies;模式族为 distribution;裁决是“保持本地除非部署、所有权或隔离证据迫使分布”;观测项包括网络往返、序列化、部分失败、边界粒度、幂等键;拒绝条件是“把远端领域对象的细粒度方法原样暴露给调用者”。
先预测:如果把 settleOrder 改成四次远程 getter,延迟和结果未知的处理路径会如何变化?先写下往返次数、可能重复的副作用和恢复动作,再阅读下面的实现;验证时应能说明一次粗粒度 DTO 调用减少了什么成本,以及它没有消除什么失败。
type OrderSummary = {
orderId: string;
totalCents: number;
status: "open" | "paid";
};
type SettlementResult = {
ok: boolean;
settlementId?: string;
reason?: "timeout" | "rejected" | "unknown";
};
interface SettlementPort {
settle(
orderId: string,
totalCents: number,
idempotencyKey: string,
): SettlementResult;
}
class SettlementRemoteFacade {
constructor(private readonly gateway: SettlementPort) {}
settleOrder(order: OrderSummary, requestId: string): SettlementResult {
const idempotencyKey = `settle:${order.orderId}:${requestId}`;
return this.gateway.settle(order.orderId, order.totalCents, idempotencyKey);
}
}这段代码把远程边界收敛为一次有业务含义的调用:订单端只发送必要 DTO,结算端口负责幂等键和结果契约。实际适配器还应设置超时、记录请求标识并区分“明确拒绝”和“结果未知”;调用者不能因为超时就盲目再次扣款。
远程调用的代价与优化
分布的第一原则是远程调用比本地调用慢得多且随时可能失败,因此必须减少网络往返,每次调用传递完成一个业务意图所需的信息。下面的对比图把本地引用、远程序列化、细粒度往返和 DTO 优化放在同一张证据图中。
Remote Facade 提供粗粒度接口,DTO 把多个字段打包成一次传输;它不是为了隐藏失败,而是为了让失败、超时和兼容版本集中在一个可测试的边界上。
选择与拒绝矩阵
| 评审问题 | 保持本地或选择分布的证据 | 应拒绝当前方案的信号 |
|---|---|---|
| 责任 | 状态所有者、业务决定者和恢复者清晰 | 两个服务都能直接改写同一份领域状态 |
| 粒度 | 一次远程调用完成一个稳定业务意图 | 客户端必须逐字段读取或逐方法修改 |
| 失败 | 超时、明确拒绝、结果未知和重试可分别观测 | 把网络异常包装成普通空值或布尔值 |
| 演化 | DTO、版本和幂等键有兼容策略 | 远程接口暴露内部对象结构并随字段变化 |
分布边界决策图
选择分布前先问部署独立性、所有权和故障隔离是否构成硬约束;若没有,保持本地通常能减少延迟与失败面。若必须分布,则用 DTO、Remote Facade、超时和幂等键把网络责任集中起来。
常见误区
可验证练习
练习
本组练习覆盖 7.1 分布对象的诱惑、7.2 远程接口和本地接口、7.3 必须使用分布的情况、7.4 关于分布边界和 7.5 分布接口。
问题 1:改写接口。 订单页面需要显示客户摘要、余额和最近订单,现有实现对远端对象逐个调用 getter。应如何改造?
问题 2:处理未知结果。 结算请求超时,但服务端可能已经完成扣款,客户端应该直接重试吗?
问题 3:判断分布边界。 两个模块部署在同一应用、共享事务且调用频率很高,团队只是因为“以后可能拆服务”就想加一层远程接口,是否应接受?
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 分布策略
把职责拆到不同进程、节点或服务,并为网络延迟与部分失败显式设计契约的架构选择。
- 部分失败
请求结果未知或仅部分参与者完成时,需要由超时、重试和恢复流程共同处理的状态。
- 粗粒度远程边界
以一次稳定业务意图组织请求和响应,减少细粒度网络往返并集中失败处理的接口边界。
- 幂等键
标识同一业务操作的稳定键,使服务端能够把安全重试识别为同一次请求而不是新动作。
本章小结
掌握第7章分布策略的标志不是背下 Remote Facade 或 DTO 的名称,而是能在订单结算跨进程调用中解释“本地摘要 → DTO → 远程外观 → 结算端口 → 可恢复结果”的责任链。学习者应利用网络往返、序列化、部分失败、边界粒度和幂等键作出可证伪的选择,并在没有真实部署或所有权约束时明确拒绝无谓分布。
前后导航
来源与改写范围
- Martin Fowler 作者图书页:核对全书主题、教程与模式参考结构。
- Martin Fowler 模式目录:核对模式名称、所属模式族和作者公开摘要。
- Pearson 出版社页面:交叉核对英文版出版信息与目录范围。