我用雪花算法生成分布式订单 ID,跑了大半年一直好好的,某天凌晨服务器自动校了一次时、把时钟往回拨了几毫秒,雪花算法当场抛异常拒绝生成 ID、订单全下不了:一次依赖"时钟一定单调向前"这个假设、却忘了机器时钟会回拨的深度复盘
那次"凌晨突发、订单全部下单失败、几分钟后又自己好了"的故障,根子是一个我从没怀疑过的假设——"时间总是向前走的"。我用雪花算法(Snowflake)生成分布式唯一订单 ID(它靠"毫秒时间戳 + 机器 ID + 序列号"拼出全局唯一、且大致递增的 ID)。跑了大半年,稳得很。可某天凌晨,突发:一段时间内所有下单都失败,日志里全是雪花算法抛的 Clock moved backwards(时钟回拨)异常;几分钟后又自己恢复了。排查发现:那台服务器在凌晨做了一次 NTP 自动校时,把系统时钟往回拨了几毫秒(它之前走快了一点,被校正回来);而雪花算法检测到"当前时间 < 上次生成 ID 的时间",为了避免生成重复/倒序的 ID,直接抛异常拒绝生成——于是这几毫秒里、以及"时钟追回到原来位置"之前的那段时间,所有要拿 ID 的下单全失败了。复盘这件事,我才真正想明白,后背发凉:问题出在雪花算法强依赖一个假设:"机器时钟一定是单调递增(只会向前、不会倒退)的",而这个假设在现实中会被打破。雪花算法用毫秒时间戳作为 ID 的高位,靠"时间向前 → ID 递增、不重复"来保证唯一性;可机器的系统时钟并不是一个绝对单调的钟——NTP 校时(把走快/走慢的钟拨回正确时间)、闰秒、虚拟机迁移/休眠唤醒、手动改时间,都可能让系统时钟突然往回跳(回拨);一旦时钟回拨,雪花算法发现"当前时间比上次还早",就没法保证 ID 不重复/不倒序了——所以它的实现通常选择抛异常拒绝生成(总比生成重复 ID 好);于是回拨期间业务拿不到 ID、全失败;我把"时钟单调向前"当成了铁律,完全没想到它会回拨,也没为回拨做任何处理。根本原因是:雪花算法强依赖"机器时钟单调递增"的假设来保证 ID 唯一,而系统时钟会因 NTP 校时/闰秒/虚拟机等回拨;回拨时算法为避免重复 ID 抛异常拒绝生成,导致业务失败;我误把"时钟一定向前"当成了不会被打破的铁律。问题的根,是依赖了"时钟单调向前"这个会被打破的假设——雪花算法靠它保证 ID 唯一,时钟回拨(NTP 校时等)时算法拒绝生成、业务失败;根源是把脆弱的基础假设当成了铁律。这篇就把这次"时钟回拨"的坑,从头到尾复盘一遍。
故障现场:时钟回拨,雪花算法拒绝生成 ID
问题在于雪花算法假设时钟单调、而时钟会回拨:
// 雪花算法的核心(简化): 靠"时间戳递增"保证ID唯一且大致有序
public synchronized long nextId() {
long now = System.currentTimeMillis();
if (now < lastTimestamp) {
// ✗ 检测到时钟回拨(当前时间 < 上次时间) → 无法保证ID不重复 → 直接抛异常!
throw new RuntimeException("Clock moved backwards. Refusing to generate id for "
+ (lastTimestamp - now) + " ms");
}
// ... 用 now(时间戳) + workerId + sequence 拼出ID ...
lastTimestamp = now;
return id;
}
/*
凌晨那次故障的时间线:
- 服务器时钟之前走快了一点点(几毫秒);
- 凌晨 NTP 自动校时, 把时钟【往回拨】几毫秒, 校正到准确时间;
- 雪花算法下一次 nextId(): now < lastTimestamp(当前比上次记录的还早) → 抛 Clock moved backwards;
- 这几毫秒 + 时钟"重新走到 lastTimestamp 之后"之前的时间里, 所有 nextId() 都抛异常;
- 下单拿不到ID → 下单失败;
- 等时钟走过了 lastTimestamp → 恢复正常。
为什么雪花算法这么"脆弱":
- 它用【毫秒时间戳】作为ID的高位, 靠"时间向前→ID递增、不同毫秒不会重复"来保证全局唯一;
- 这隐含了一个【强假设】: 机器时钟单调递增(只向前, 永不倒退);
- 一旦时钟回拨, "当前时间<上次时间", 同一个时间戳可能被用两次 → ID重复/倒序;
- 为避免生成重复ID(那是更严重的灾难), 实现通常选择"回拨就抛异常拒绝生成"。
机器时钟为什么会回拨(它不是单调钟!):
- NTP校时: 系统时钟走快/走慢了, NTP把它拨回正确时间——可能往回拨;
- 闰秒: 极少数情况下时间被调整;
- 虚拟机迁移/休眠唤醒: 时钟可能跳变;
- 手动改系统时间; 时钟源切换。
★ 核心: 雪花算法依赖"时钟单调递增"来保证ID唯一, 但机器的【墙上时钟(wall clock)】会回拨
(NTP/闰秒/虚拟机/手动); 回拨时算法拒绝生成→业务失败; 别把"时钟一定向前"当成不会被打破的铁律。
看着"Clock moved backwards"的异常和"几毫秒后自己恢复"的规律,我又懊恼又后怕:"我从来没怀疑过'时间是向前走的'这件事——这不是天经地义吗?谁能想到服务器校个时,时钟还能往回跳几毫秒,把雪花算法整崩了。我把一个'会偶尔被打破'的物理假设,当成了'永远成立'的铁律。"这个坑最隐蔽的地方在于:它依赖的假设("时钟单调向前")在 99.99% 的时间里都成立,跑大半年都没事,给你"这假设铁打不变"的强烈错觉;它只在"时钟回拨"这种罕见、外部触发(NTP 校时)、且转瞬即逝的时刻才暴露;而且"几分钟自己好了"让人容易当成偶发抖动、不深究。下面就来拆解,时钟回拨和分布式 ID 该怎么处理。
第一件事:搞懂时钟回拨与对它的依赖
我顺着这次事故,把时钟回拨和分布式 ID 的应对彻底理清了。
为什么会时钟回拨? 依赖时钟的系统怎么应对?
【核心: 机器墙上时钟会回拨(NTP校时/闰秒/虚拟机/手动)、不是单调钟; 雪花算法等依赖"时钟单调递增"的
系统在回拨时会出问题; 应对: 等时钟追上/借位/单调时钟/不依赖时钟的ID方案; 别把脆弱的基础假设当铁律】
1. 墙上时钟(wall clock) vs 单调时钟(monotonic clock):
- 墙上时钟(System.currentTimeMillis): 反映"真实世界的时间", 会被校正(NTP)、会回拨、会跳变;
- 单调时钟(System.nanoTime): 只保证"单调递增"(适合测耗时), 但不代表真实时间、不能跨机器/重启比较;
- 雪花算法用的是墙上时钟(要真实时间做ID高位), 所以会被回拨影响。
2. 时钟为什么会回拨(墙上时钟不可靠):
- NTP校时: 你的机器钟走快/走慢, NTP拨回准时间(可能往回拨, 默认会"跳"或"渐进调整");
- 闰秒、虚拟机迁移/休眠唤醒、手动改时间、时钟源问题。
3. 雪花算法应对时钟回拨的方案:
① 小幅回拨: 等待时钟追上(回拨几毫秒, 就sleep等now>lastTimestamp再生成)——简单, 牺牲短暂可用性;
② 借用序列号/时间位: 回拨时用"上次时间戳+序列号继续"或预留的位, 撑过小回拨;
③ 备用workerId/缓存时间: 一些扩展方案(如百度uid-generator、美团Leaf)有更完善的回拨处理;
④ 大幅回拨(几秒以上): 一般直接告警 + 该节点停止发号(避免重复), 靠其他节点顶上。
4. 更稳的分布式ID方案(不强依赖时钟单调):
- 号段模式(Leaf-segment): DB分配一段ID区间, 本地发号, 用完再取下一段——不依赖时钟;
- Redis INCR / 数据库自增: 集中发号, 不依赖各机器时钟(但有单点/性能考量);
- UUID: 完全不依赖时钟和中心(但无序、占空间);
- 按业务权衡: 要趋势递增+高性能用雪花(处理好回拨), 要绝对可靠用号段, 等等。
5. 本质: 别把"基础设施/物理层面看似理所当然的保证"当成永远成立的铁律
- "时钟单调向前""时间准确""网络可靠""机器不宕"——这些"看起来天经地义"的假设, 都会被偶尔打破;
- 依赖一个假设(尤其后果严重时), 要确认它真的总成立; 不成立时(回拨/不可靠), 要有应对。
一句话: 机器墙上时钟会回拨(NTP/闰秒/虚拟机)、不是单调钟; 雪花算法依赖"时钟单调递增"、回拨时拒绝发号致业务失败;
应对用等待追上/借位/成熟方案, 或用不依赖时钟的ID(号段/Redis); 别把"时钟一定向前"等脆弱假设当铁律。
这套认知,是整个坑的根。墙上时钟 vs 单调时钟:墙上时钟(currentTimeMillis)反映真实时间、会被 NTP 校正回拨;单调时钟(nanoTime)只保证递增但不是真实时间;雪花算法用墙上时钟,所以受回拨影响。为什么回拨:NTP 校时、闰秒、虚拟机迁移/唤醒、手动改时间。雪花算法应对:小幅回拨等时钟追上、借用序列号/时间位、用成熟扩展方案(Leaf/uid-generator)、大幅回拨告警停止发号。更稳的 ID 方案:号段模式、Redis INCR/DB 自增、UUID——按业务权衡。本质:别把"基础设施/物理层面看似理所当然的保证"(时钟单调、时间准确、网络可靠、机器不宕)当成永远成立的铁律,它们会被偶尔打破。一句话:机器墙上时钟会回拨(NTP/闰秒/虚拟机)、不是单调钟;雪花算法依赖"时钟单调递增"、回拨时拒绝发号致业务失败;应对用等待追上/借位/成熟方案,或用不依赖时钟的 ID(号段/Redis);别把"时钟一定向前"等脆弱假设当铁律。
第二件事:正解——处理回拨 + 必要时用不依赖时钟的方案
知道了时钟会回拨,正解就清楚了:对小幅回拨优雅处理,或用不强依赖时钟的 ID 方案。
// 正解1: 小幅回拨 → 等待时钟追上(简单, 牺牲极短可用性)——比直接抛异常友好
public synchronized long nextId() {
long now = System.currentTimeMillis();
if (now < lastTimestamp) {
long offset = lastTimestamp - now;
if (offset <= 5) { // 小幅回拨(如<=5ms): 等时钟追上再发号
try {
wait(offset << 1); // 等待
now = System.currentTimeMillis();
if (now < lastTimestamp) throw new RuntimeException("时钟仍回拨");
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
} else {
// 大幅回拨: 告警 + 该节点停止发号(别冒险生成重复ID), 靠其他节点顶
throw new ClockBackwardsException("时钟大幅回拨 " + offset + "ms, 本节点停止发号");
}
}
// ... 正常拼ID ...
lastTimestamp = now;
return id;
}
// 正解2: 用预留位/序列号撑过回拨(成熟方案的思路)
// - 借鉴 美团Leaf-snowflake / 百度uid-generator: 用zk/db记录上次时间、检测回拨、用未来时间或备用位;
// - 别自己手搓裸雪花算法上生产, 用经过生产验证的实现。
// 正解3: 不强依赖时钟的分布式ID方案(从根上避开回拨)
// - 号段模式(Leaf-segment): 从DB一次取一段ID区间(如[1000,2000)), 本地发号, 用完再取下一段;
// → 不依赖各机器时钟, 高性能, 趋势递增; 是很多场景更稳的选择;
// - Redis INCR: 集中原子自增发号(注意持久化和单点);
// - 数据库自增 / 多DB步长; UUID(无序但绝不依赖时钟)。
// → 按需求选: 要时间有序+高性能用雪花(处理好回拨), 要绝对稳健用号段。
// 正解4: 运维侧也要配合
// - NTP用"渐进式校正(slew)"而非"跳变(step)", 让时钟平滑调整、减少回拨幅度;
// - 监控时钟回拨告警; 关键发号节点的时间同步要稳。
// 反例(别这样):
// - 裸用雪花算法、回拨直接抛异常、不做任何处理 → 校时就全挂(本次);
// - 假设"时钟永远向前", 整个ID方案没考虑回拨。
// 核心: 处理时钟回拨(小幅等待追上、大幅告警停发)、用成熟实现; 或改用不依赖时钟的ID方案(号段/Redis);
// 运维侧用渐进式NTP校正+回拨监控; 别假设时钟永远单调向前。
这套正解的关键,是承认"时钟会回拨"这个事实,并为它做处理,或干脆不把唯一性押在"时钟单调"上。小幅回拨等待追上:回拨几毫秒就 sleep 等时钟追上再发号,比直接抛异常友好——这正是本次缺的。大幅回拨告警停发:别冒险生成重复 ID,该节点停发、靠其他节点顶。用成熟实现:Leaf/uid-generator 等已处理好回拨,别自己手搓裸雪花上生产。用不依赖时钟的方案:号段模式从 DB 取 ID 区间本地发号,不依赖时钟、高性能、趋势递增,是很多场景更稳的选择。运维配合:NTP 用渐进式校正(slew)、监控时钟回拨告警。
第三件事:其他几个"依赖了脆弱的基础假设"的坑
顺着这次时钟回拨,我把"依赖了看似可靠、实则会被打破的基础假设"的几类坑也一并理了:
几类"依赖脆弱基础假设"的坑(都是"把会偶尔被打破的假设当成铁律"):
坑1: 假设"时钟单调向前"(本篇)——会回拨; 正解: 处理回拨/用单调时钟测耗时/不依赖时钟发号。
坑2: 假设"两台机器时钟一致"——时钟有漂移, 不能用各机器本地时间戳比较先后/做分布式排序;
正解: 用逻辑时钟(Lamport/向量时钟)、或中心化时间源, 别比墙上时钟。
坑3: 假设"网络可靠/不丢包/低延迟"(分布式八大谬误)——网络会丢、会延迟、会分区;
正解: 超时、重试(带退避583)、幂等(586)、容错。
坑4: 假设"机器不宕、磁盘不坏、进程不挂"——都会发生;
正解: 冗余、备份、健康检查、故障转移。
坑5: 假设"ID/序列永不回绕、空间永不耗尽"——自增ID会到上限、端口/句柄会耗尽;
正解: 用足够大的类型、监控用量、设计回收。
坑6: 假设"配置/依赖在启动时就绪且不变"(同608/595)——会变、会延迟就绪;
正解: 热更新、等待就绪、容忍变化。
共同的根: 系统总会依赖一些"基础设施/物理/环境层面的保证"(时钟单调、时间一致、网络可靠、机器不宕、
资源不耗尽); 这些保证"绝大多数时候成立", 容易被当成"永远成立的铁律"; 但它们都会被偶尔打破——
依赖一个假设(尤其打破后果严重时), 要确认它真的总成立, 并为"它被打破"准备好应对, 而非盲目信任。
这些坑看似不同,根却是同一个:系统总会依赖一些"基础设施/物理/环境层面的保证"(时钟单调、时间一致、网络可靠、机器不宕、资源不耗尽);这些保证"绝大多数时候成立",容易被当成"永远成立的铁律",但它们都会被偶尔打破。认清这个根("识别自己依赖的基础假设、确认它真的总成立、为被打破做准备"),才不会被"从没出过事"的脆弱假设坑到。
第四件事:分布式 ID 方案 / 时钟类型——两张对照表
我把常见分布式 ID 方案、以及两种时钟,整理成对照表,贴在了团队的架构规范里:
| ID 方案 | 依赖时钟吗 | 特点 |
|---|---|---|
| 雪花算法 Snowflake | 是(墙上时钟) | 趋势递增、高性能,怕回拨 |
| 号段模式 Leaf-segment | 否 | DB 分段、本地发号,稳、趋势递增 |
| Redis INCR | 否 | 集中自增,注意持久化/单点 |
| 数据库自增 | 否 | 简单,有性能/扩展瓶颈 |
| UUID | 否 | 无中心、不依赖时钟,但无序占空间 |
| 时钟 | 保证 | 用途 |
|---|---|---|
| 墙上时钟 currentTimeMillis | 真实时间,但会回拨/跳变 | 显示时间、雪花 ID(怕回拨) |
| 单调时钟 nanoTime | 只保证单调递增 | 测耗时,不能跨机器/重启比 |
| 逻辑时钟 Lamport/向量 | 表达事件先后 | 分布式排序、因果关系 |
这两张表的核心,第一张是雪花算法是唯一强依赖时钟的,怕回拨;号段模式等不依赖时钟、更稳;第二张是墙上时钟会回拨(雪花用它才出问题),测耗时该用单调时钟,分布式排先后该用逻辑时钟——别拿墙上时钟做它不擅长的事。记住一条:要趋势递增 + 高性能用雪花(但必须处理回拨),要绝对稳健省心用号段。
第五件事:关于时钟与分布式 ID 的几组容易想当然的认知
这次事故也让我厘清了几组关于时钟的、容易想当然的概念:
| 直觉以为 | 实际上 |
|---|---|
| 时间总是向前走的 | 墙上时钟会回拨(NTP/闰秒/虚拟机) |
| 雪花算法跑大半年没事就没问题 | 只是没遇到回拨,校时就暴露 |
| 校时是好事,不会有副作用 | 校时可能往回拨,坑了依赖时钟的系统 |
| currentTimeMillis 单调递增 | 不单调,会回拨;测耗时用 nanoTime |
| 各机器时间戳能直接比先后 | 时钟有漂移,不能比;用逻辑时钟 |
| 雪花算法 = 分布式 ID 标准答案 | 是选项之一,怕回拨;号段等也很好 |
| "几分钟自己好了"是偶发抖动 | 常是时钟回拨/可恢复的确定性问题 |
这张表里,我栽的是第一行和第二行:把"时间总是向前走"当成了天经地义的铁律,又因为"雪花算法跑大半年没事"而深信不疑,完全没为时钟回拨做任何准备。厘清这些,核心是一个意识:"时钟单调向前"这种我们最不假思索的物理直觉,在真实的机器上其实是会被打破的(NTP 校时等);任何把系统正确性押在这类"脆弱的基础假设"上的设计,都要为"假设被打破的那一刻"准备好应对——要么处理它,要么改用不依赖这个假设的方案。
第六件事:用时钟 / 做分布式 ID 时,我现在的自检习惯
现在每当我要用时钟、或设计依赖某个基础假设的系统,我都会先按这张图问自己:
这张图的精髓,是"识别我押在哪些脆弱基础假设上(时钟单调等)、为它被打破做准备、能不依赖就别依赖"。先问正确性是否押在时钟单调上、处理回拨没、能否改用不依赖时钟的方案、再盘点其他想当然的基础假设。这套习惯,让我从"默认时钟永远向前"变成了"识别并为脆弱假设兜底"——核心始终是:机器墙上时钟会回拨、不是单调钟;雪花算法依赖时钟单调、回拨时拒绝发号致业务失败;应对用等待追上/借位/成熟方案或不依赖时钟的 ID;别把"时钟一定向前"等脆弱假设当铁律。
我立下的几条规矩
这场"时钟回拨、雪花算法拒绝发号"的事故,换来了我做架构时,刻进骨子里的几条铁律:
- 机器墙上时钟(currentTimeMillis)会回拨(NTP 校时/闰秒/虚拟机/手动),不是单调钟。
- 雪花算法靠"时钟单调递增"保证 ID 唯一,回拨时为避免重复 ID 会抛异常拒绝发号、业务失败。
- 处理回拨:小幅回拨等时钟追上、大幅回拨告警 + 该节点停止发号(别冒险生成重复 ID)。
- 用经过生产验证的成熟实现(Leaf/uid-generator),别自己手搓裸雪花上生产。
- 对稳健要求高,改用不依赖时钟的方案:号段模式、Redis INCR、数据库自增。
- 测耗时用单调时钟 nanoTime;分布式排先后用逻辑时钟;别拿墙上时钟做它不擅长的事。
- 盘点系统依赖的基础假设(时钟单调/一致、网络可靠、机器不宕、资源不耗尽),为它们被打破做准备。
附:模拟时钟回拨、验证发号器的健壮性
借这次的坑,我给发号器加了一个"可注入的时钟",这样就能在测试里主动模拟时钟回拨,验证它真的扛得住,而不是等到线上 NTP 校时才发现问题。
// 让时钟可注入(而非硬编码 System.currentTimeMillis), 便于测试模拟回拨
interface Clock { long now(); }
class SystemClock implements Clock {
public long now() { return System.currentTimeMillis(); }
}
class IdGenerator {
private final Clock clock;
private long lastTimestamp = -1;
IdGenerator(Clock clock) { this.clock = clock; }
synchronized long nextId() {
long now = clock.now();
if (now < lastTimestamp) {
long offset = lastTimestamp - now;
if (offset <= 5) { /* 小幅: 等待追上 */ now = waitUntil(lastTimestamp); }
else throw new ClockBackwardsException("回拨过大: " + offset + "ms");
}
lastTimestamp = now;
return now /* << ... | workerId | seq */;
}
}
// 测试: 用一个"可控时钟"主动模拟回拨, 验证发号器的行为
@Test void 时钟小幅回拨_应等待而非崩溃() {
FakeClock clock = new FakeClock(1000L);
IdGenerator gen = new IdGenerator(clock);
long id1 = gen.nextId();
clock.set(998L); // 主动把时钟往回拨2ms!
long id2 = gen.nextId(); // 应"等待追上"后正常发号, 而非抛异常
assertTrue(id2 > id1); // 验证: 回拨后发的号仍然递增、没崩
}
// FakeClock: 一个可手动 set 时间的 Clock 实现, 让"回拨"在测试里可复现。
// 原则: 把"会变、不可控的外部依赖"(时钟)抽象成可注入的接口, 就能在测试里【主动制造它的异常情况】
// (回拨、跳变), 在上线前验证系统扛不扛得住 —— 而不是把"它会不会出问题"赌在生产环境上。
这个"可注入时钟"的价值,在于它把"时钟回拨"这个原本只能在生产环境偶然遇到的异常,变成了测试里可主动复现、可验证的场景。它体现了一个更普适的工程方法:把那些"会变、不可控、偶尔出异常"的外部依赖(时钟、网络、随机数)抽象成可注入的接口,你就能在测试里主动制造它们的异常情况,在上线前验证系统扛不扛得住——而不是把"万一它出问题"赌在生产环境上,等真出事了才知道。
写在最后
回头看,这场由"时钟回拨"引发的、雪花算法拒绝发号致全站下单失败的事故,真正教给我的,远不止"处理时钟回拨、用号段模式"这一个技巧。它让我对"我们的系统, 总是悄无声息地建立在一堆'我们从不质疑的基础假设'之上(时钟向前走、网络通、机器在); 这些假设'太基础、太天经地义'了, 以至于我们根本意识不到自己在依赖它们, 更不会为'它们万一不成立'做准备——直到某天它被打破, 我们才发现自己的大厦原来建在一个我们以为是磐石、实则会偶尔晃动的地基上",有了一次刻骨的体会。我栽跟头,是因为"时间向前走"这个假设太底层、太理所当然了——它如此天经地义, 以至于我从未把它当成一个"假设", 而是当成了"宇宙的铁律";正因为我意识不到自己在依赖它, 我自然也不会去想"它会不会不成立"、更不会为它的"不成立"做任何准备;于是当 NTP 把时钟轻轻往回拨了几毫秒——这个假设被打破的那一刻——我那座建立在"时间单调向前"之上的发号大厦, 就毫无防备地塌了。这让我领悟到一个关于"基础假设与隐性依赖"的深刻认知:越是"基础、底层、天经地义"的假设(时钟向前、网络可靠、机器不宕、磁盘不坏、内存够用), 我们越容易意识不到自己正在依赖它——它们沉默地、隐性地支撑着我们的系统, 而我们浑然不觉;恰恰是这些"隐性的、被默认的依赖", 最危险: 因为我们没意识到它的存在, 就不会去验证它、不会为它的失效做准备; 一旦它被打破(而所有基础保证都会偶尔被打破), 系统就在一个"我们从没设防的方向"上崩溃;所以真正的健壮, 来自于把那些"不言自明的基础假设"显式地翻出来、承认"它只是个假设, 会被打破", 并为打破做准备——这, 比应对那些"我们已经知道要小心"的风险, 更难、也更重要。这给了我一种审视系统时的清醒:每当我设计或依赖一个系统时,要刻意地去挖掘那些"隐性的、我没意识到自己在依赖的基础假设"——问自己"我这套东西, 默默地建立在哪些'我以为永远成立'的前提上?(时间、网络、硬件、资源……)这些前提真的永远成立吗?万一不成立, 会怎样?"——把隐性依赖显性化, 为最基础的假设也准备好"它被打破时"的应对;"识别并显式化那些不言自明的基础假设、承认它们会被打破并为之设防",是构建真正健壮系统的深层功夫。认清系统建立在隐性的基础假设上、越基础越天经地义的假设越容易被忽略、要把隐性依赖显性化并为其被打破设防——这,是我用一次时钟回拨的事故,换来的、关于架构、也关于如何审视隐性依赖的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次用雪花算法前,先想一想"万一时钟回拨呢"、加上回拨处理或换个不依赖时钟的方案,那我对着那个凌晨突发、几分钟自愈的"Clock moved backwards"排查的这段时间,就值了。
—— 别看了 · 2026