🌐 本章核心:分布式系统最难处理的不是组件一定会失败,而是发生部分故障时,观察者无法确定远端究竟发生了什么。超时只表示“不知道”,本地认知不等于全局真相。 学习日期:2026-09-16 学习方式:3 次互动精讲 + 支付与订单案例 + Redis/数据库租约设计 + 情境问答 + 综合测验 学习结果:最终测验通过,能够分析超时、重试、时钟、进程暂停、租约、fencing token、脑裂、多数派,以及安全性与活性的取舍。
一、全章主线
第 8 章不直接给出某个万能算法,而是建立设计分布式系统之前必须接受的现实:
- 网络不可靠:请求和响应可能丢失、延迟或排队。
- 时钟不可靠:不同机器的物理时钟会漂移、回拨或跳跃。
- 进程会暂停:GC、虚拟化、I/O 和调度可能让线程长时间停止。
- 节点无法独自判断真相:节点存活不代表仍然拥有领导权。
- 算法依赖系统模型:必须明确允许哪些故障,以及要保持哪些安全性和活性。
速记:超时表示“不知道”,不是“失败”;本地认知不等于全局真相。
二、部分故障与不可靠网络
单机程序通常表现为整体工作或整体失败;分布式系统则可能只有一部分组件异常,其余部分继续运行。
当订单服务调用支付服务但没有收到响应时,至少存在以下可能:
- 请求在途中丢失。
- 请求仍在网络、操作系统或线程池队列中。
- 支付服务暂时暂停或已经崩溃。
- 支付已经成功,但响应丢失。
- 支付已经成功,响应只是延迟。
- 订单服务自身过载,未能及时处理响应。
调用方看到的现象完全相同:截止时间内没有响应。因此,超时是一种本地判断,不能证明远端操作失败,也不能证明节点死亡。
三、超时、重试与幂等
| 策略 | 收益 | 风险 |
| 较短超时 | 更快怀疑真正故障 | 误判慢节点,引发重试与故障转移 |
| 较长超时 | 降低误判概率 | 用户等待和真正故障恢复更慢 |
| 动态故障检测 | 根据延迟和抖动调整怀疑程度 | 仍然无法证明节点已经死亡 |
流量高峰中,服务响应时间从 100 ms 上升到 800 ms,而客户端在 300 ms 超时并立即重试三次,会形成:
负载升高 → 响应变慢 → 大量超时 → 大量重试
↑ ↓
└──────── 负载进一步升高 ──────┘
工程规则:
- 使用指数退避、随机抖动、最大次数和总截止时间。
- 区分一次请求超时与两次尝试之间的退避时间。
- 只重试临时故障,不盲目重试业务拒绝和参数错误。
- 同一次业务操作的所有重试使用相同幂等键。
- 幂等防止重复业务效果,但不会消除重复计算、连接和数据库压力。
- 总截止时间应向下游传播,不能让每一层重新获得完整预算。
四、支付幂等
支付请求示例:
POST /payments
Idempotency-Key: pay-O-123
支付服务应保证:
- 同一业务操作的所有重试使用同一个键。
- 幂等键具有唯一约束。
- 首次成功后持久化处理结果。
- 后续相同键返回原结果,不再次扣款。
- 相同键携带不同金额或参数时拒绝请求。
仅使用内存锁不能保证幂等:进程重启会丢锁,多实例之间不共享锁,长时间暂停可能让租约过期。对于本地业务,应让幂等记录和业务写入处于同一数据库事务;对于第三方支付,应使用渠道幂等键、PENDING/UNKNOWN 状态、结果查询和对账。
五、墙上时钟与单调时钟
| 时钟 | 适合用途 | 主要风险 |
| 墙上时钟 time-of-day | 日志、展示、日历规则 | 漂移、NTP 校时、回拨和跳跃 |
| 单调时钟 monotonic | RPC 耗时、超时、退避 | 不同机器的数值不能直接比较 |
墙上时钟只能表达“这台机器认为现在是几点”,不能严格证明跨节点事件顺序。NTP 可以减小误差,但不能让误差归零。
如果使用物理时间戳执行 Last Write Wins,时钟较慢节点产生的真实新写入可能拥有更小时间戳,从而被静默丢弃。
测量持续时间必须使用单调时钟。若墙上时钟在任务期间向后调整 2 秒,两次读数相减会少算 2 秒,甚至得到负数。
六、进程暂停、租约与 fencing token
进程可能因为 GC、操作系统调度、虚拟机暂停、缺页或 I/O 长时间停止。暂停期间租约可能过期;进程恢复后却沿着旧调用栈继续执行,成为僵尸客户端。
Worker A 获得 token=41
A 长时间暂停,租约过期
Worker B 获得 token=42 并完成写入
A 恢复,继续尝试写入
最终资源发现 41 < 42,拒绝 A
关键结论:
- 租约只是在一段时间内授予执行资格。
- 写入前由客户端检查一次租约不够,检查后仍可能再次暂停。
- 每次重新授予租约时生成严格递增的 fencing token。
- 最终资源保存最后接受的 token,并原子拒绝旧 token。
数据库条件写入骨架:
UPDATE protected_resources
SET value = ?, last_fencing_token = ?
WHERE id = ?
AND last_fencing_token < ?;
受影响行数为 0 时,Worker 必须认为自己已经过期并停止操作。
七、Redis 软锁与数据库权威租约
Redis 可以使用以下形式减少并发和惊群:
SET soft-lock:{order}:123 <owner_uuid> NX PX 30000
但 Redis 锁不应成为关键业务正确性的唯一来源。推荐职责划分:
- Redis:软锁、TTL、owner UUID,减少重复执行和数据库竞争。
- 数据库:权威 lease、单调 fencing token、最终条件写入。
完整流程:
- Worker 生成稳定的 acquire ID。
- 获得 Redis 软锁。
- 在数据库事务中锁定资源租约行;租约为空或过期时递增 token。
- 执行业务时携带 token。
- 数据库只接受比当前记录更新的 token。
- 续租和释放都必须校验 owner ID 与 token。
- Redis 主从切换或软锁失效时,数据库仍只承认权威租约。
如果所有数据都位于同一个数据库且竞争不大,直接使用数据库事务、行锁或乐观版本号通常更简单。
八、多次有序写入
同一租约中的多次写入可以使用组合版本:
(fencing_token, operation_sequence)
例如:
(42, 1) < (42, 2) < (43, 1)
如果后一个写入是完整状态,可以按组合版本保留最新状态。如果每个操作都必须执行,则需要:
- 每个操作拥有稳定 operation ID。
- 保存操作日志和执行结果。
- 强制 sequence 等于 last sequence + 1。
- 业务修改、操作日志和 sequence 推进处于同一事务。
- 同一资源尽量只允许一个写请求在途。
典型严格顺序场景包括有余额约束的取款、订单状态机、数据库迁移和增量补丁。
九、多数派、脑裂与任期
5 节点集群发生网络分区:
多数派:A、B、C
少数派:D、E
A、B、C 可以得到 3 票并选出新 Leader;D 即使仍然运行,也不能继续安全提交写入。
多数派的关键性质是任意两个多数派必定相交。共识协议使用 term、epoch 或类似 fencing token 的机制拒绝旧 Leader:
旧 Leader D:term=7
新 Leader A:term=8
D 恢复后携带 term=7 的写入必须被拒绝
如果少数派也向客户端确认写入,会产生脑裂、日志分叉、写入丢失,并可能破坏库存、余额和唯一性等业务不变量。
十、系统模型
| 模型 | 假设 |
| Crash-stop | 节点崩溃后不再恢复 |
| Crash-recovery | 节点可能重启,依赖持久化状态恢复 |
| Byzantine | 节点可能撒谎、发送矛盾消息或恶意行为 |
| 同步网络 | 网络与处理时间存在已知上限 |
| 部分同步网络 | 通常延迟有界,异常时可能任意长 |
| 异步网络 | 不假设延迟或暂停存在固定上限 |
普通数据库通常按非拜占庭、部分同步环境设计:节点可能崩溃、暂停和断网,但不会故意构造谎言。
十一、安全性与活性
| 性质 | 含义 | 示例 |
| Safety 安全性 | 坏事永远不发生 | 不重复扣款;旧 token 不能覆盖新 token |
| Liveness 活性 | 好事最终会发生 | 最终选出 Leader;网络恢复后继续服务 |
网络分区时,少数派拒绝写入会损失活性或可用性,但仍保持安全性;两个分区都提交冲突写入则直接破坏安全性。
十二、学习问答与纠偏
- 正确判断:支付请求超时不能证明支付失败。
- 正确提出:使用稳定事务 ID 或幂等键重试,避免重复扣款。
- 补全理解:幂等记录与本地业务写入应由数据库唯一约束和事务共同保护,不能只靠内存锁。
- 正确识别:客户端立即重试会放大负载,形成重试风暴。
- 补全理解:指数增长的主要是退避时间;单次超时应由真实延迟分布和总截止时间决定。
- 正确区分:RPC 耗时使用单调时钟,日期展示使用墙上时钟。
- 曾将时钟向后调整理解为耗时增加;修正为计算耗时减少,甚至可能为负数。
- 正确识别:旧 Worker 租约过期后没有权利继续写入。
- 正确理解:fencing token 必须由最终数据库强制检查。
- 正确判断:5 节点分区中 3 节点一侧可以形成多数派。
- 补全理解:少数派旧 Leader 的写入不是“数据分区”,而是可能导致脑裂、日志分叉和已确认写入丢失。
- 最终综合题通过:能够为重复支付、僵尸 Worker、脑裂和响应丢失分别选择幂等、fencing token、多数派与权威状态查询。
十三、最终综合案例
订单 O-123 的处理过程:
- W1 获得 10 秒租约和 token 41。
- W1 用幂等键 pay-O-123 调用支付;支付成功但响应延迟。
- W1 超时并发生 15 秒 GC 暂停。
- 协调服务发生 3/2 网络分区,多数派向 W2 发放 token 42。
- W2 使用相同幂等键查询或重试支付,获得原成功结果。
- W2 用 token 42 将订单更新为 PAID。
- W1 恢复后携带 token 41 写入,被数据库拒绝。
- 数据库提交响应丢失时,调用方使用相同 operation ID 重试或查询权威状态。
| 风险 | 防护机制 |
| 重复支付 | 支付渠道幂等键、参数校验和对账 |
| 僵尸 Worker | 租约、fencing token、最终资源条件写入 |
| 脑裂 | 多数派、任期号和旧 Leader 拒绝机制 |
| 响应丢失 | 相同 operation ID 的幂等重试与权威状态查询 |
十四、工程检查清单
- 能解释部分故障为什么比完全故障更难处理
- 能说明超时为什么只能表达怀疑
- 能设计有限重试、退避、抖动和总截止时间
- 能区分墙上时钟和单调时钟
- 能解释进程暂停如何产生僵尸客户端
- 能使用 fencing token 保护最终资源
- 能说明 Redis 锁与数据库权威租约的职责边界
- 能解释多数派如何避免两个冲突决定同时提交
- 能区分安全性问题和活性问题
十五、一句话复盘
分布式系统设计的核心不是消除所有故障,而是在无法确定远端状态的情况下,仍然用幂等、版本、fencing、事务和多数派守住用户依赖的安全保证。