时钟回拨

时钟回拨是雪花算法的主要挑战之一。

前面几页一路都在讲”本地生成”,但整个方案的地基其实是本机时钟。凌晨两点运维手动校了一次 NTP,机器时间被拨回了两秒——对业务代码毫无影响,对雪花算法却是灾难:时间戳变小了,新生成的 ID 反而比之前生成的更小,一旦序列号也重合,就会直接发出重复的 ID。

这一章要回答三个问题:回拨多少算严重、多大的回拨可以等一等、更大的回拨是否干脆拒绝发号。轻微回拨可以等待时钟追平,幅度较大时切到备用时间源,最坏的情况是直接拒绝发号并告警。思路是一致的:宁可让发号短暂停顿,也不要发出一个可能重复的 ID。

读完这一章,你应该能判断一个具体的回拨场景该用哪种策略,并说明为什么”等待时钟追上”在回拨幅度较大时会把请求堆在服务里,以及为什么这类故障一定要配告警而不是只打日志。

问题分析

什么是时钟回拨

系统时间向后调整,导致时间戳变小。

影响

时间  T1  T2  T3  T4
ID    1   2   3   2  ← 重复!

解决方案

1. 等待时钟追上

当检测到当前时间戳小于上一次使用的时间戳,先不急着发号,而是让生成逻辑停在这里,等到系统时间追平到那个水位之后再继续。

检测到 ts < lastTimestamp
  → 进入等待,sleep 1ms 后重试
  → 期间不发放任何 ID
  → 直到 ts >= lastTimestamp,恢复正常发号

这种做法适合回拨幅度很小的情况,比如几毫秒到几十毫秒。优点是实现最简单,不需要任何额外组件;缺点是等待期间该节点的请求会被阻塞,回拨幅度越大,堆积的请求越多。


2. 使用备用时间源

当回拨幅度大到等不起时,可以引入一个不依赖本机时钟的时间源继续发号。常见的做法有三种:使用备用 worker,在另一个时钟正常的节点上代为生成;使用逻辑时钟,在检测到回拨时对本机时间做一次偏移补偿,保证时间戳单调递增;使用时钟回拨标记位,把”发生过回拨”这个事实编码进 ID 的空闲位里。

方案 A:备用 worker
  本机发现回拨 → 把发号请求转交给时钟正常的备用节点 → 拿回 ID

方案 B:逻辑时钟
  检测到回拨 → lastTimestamp += 回拨幅度 → 继续用补偿后的时间戳

方案 C:回拨标记位
  占用 ID 中的空闲位做标记 → 消费方据此识别异常期生成的 ID

选哪一种,取决于业务能不能接受”时间戳不再等于真实时间”,以及有没有备用节点可以承接。


3. 抛出异常

当回拨幅度大到无法等待、也没有备用时间源可用时,唯一安全的做法就是拒绝发号:抛出异常或者返回明确的错误码,让调用方知道 ID 服务暂时不可用。

这是一个用可用性换正确性的取舍。ID 通常是主键,重复主键会污染数据,而且往往要靠数据修复才能收拾干净;相比之下,主键服务短暂停发只会让写入链路失败,业务侧可以重试。宁可暂停,也不要发出去一个可能重复的 ID。

拒绝发号时必须同时告警。回拨不会凭空发生,它通常意味着 NTP 故障、虚拟机迁移或者宿主机时间被调整,需要人工介入处理,否则这个节点会一直拒绝发号。


最佳实践

结合多种方案,提高可靠性。


下一步

了解了时钟回拨后,我们学习机器 ID 分配。

👉 下一节:机器 ID 分配

发现内容有问题?

告诉我们哪里需要改进。