雪花算法的时钟回拨怎么解决?
作者简介:大家好,我是码炫码哥,前中兴通讯、美团架构师,现任某互联网公司CTO,兼职码炫课堂主讲源码系列专题
代表作:《jdk源码&多线程&高并发》,《深入tomcat源码解析》,《深入netty源码解析》,《深入dubbo源码解析》,《深入springboot源码解析》,《深入spring源码解析》,《深入redis源码解析》等
联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬。码炫课堂的个人空间-码炫码哥个人主页-面试,源码等
释放21集全网最深ConcurrentHashMap的vip视频,复现每一行源码
回答
由于雪花算法高度依赖系统时钟,当时钟回拨发生时,基于时间戳的分布式 ID 生成器雪花算法就可能会出现重复 ID 或者全局递增性被破坏的问题,这就直接威胁到系统的一致性和稳定性。
解决时钟回拨问题,有几种常见的方法:
- 直接抛出异常。不管
3 * 7 = 21,直接抛出异常,将问题交给调用方去处理。 - 延迟等待。将当前阻塞,知道系统时间重新追回上之前的时间戳。
- 切换机器。利用多节点部署特性,这台机器有问题,切换到其他正常的机器上。
- 追赶时钟。你不是慢么?那我就加速追赶时间。
详解
为什么会有时钟回拨问题?
从根本上来说,时钟回拨的根本原因在于服务器的物理时间不总是精确同步,完全可靠的,而雪花算法严重依赖时间戳这一生成 ID 的核心元素。
常见的时钟回拨原因:
- NTP 时间同步异常:NTP(网络时间协议)用于同步服务器时间。如果服务器与时间源同步时,检测到系统时间比当前标准时间快,则会将系统时间回调到正确的时间点。
- 手动时间调整:运维人员可能会因为错误的系统设置、误操作或调整时区而修改系统时间。
- 虚拟化环境的问题:在虚拟机或容器化部署中,宿主机时间的改变会直接影响虚拟机的时间,从而导致时钟回拨。
为什么时钟回拨会成为问题?
由于雪花算法依赖时间戳生成唯一的 ID,当时钟回拨发生时,新的时间戳小于之前生成 ID 的时间戳。这样会导致:
ID 冲突问题
雪花算法的 ID 由时间戳、机器 ID 和序列号组成。如果时钟回拨导致当前时间戳小于上次生成的时间戳,可能在同一时间戳范围内产生重复的序列号,从而生成重复的 ID。例如:
- 之前的时间戳为
1673512020000,生成了序列号 1。 - 时钟回拨后,时间戳又回到
1673512020000,序列号重新开始,生成了重复的 ID。
全局递增性失效
如果生成的 ID 不再递增,某些依赖 ID 递增性的业务(如数据库主键)可能会发生问题:
- 数据库的分区键或索引可能因 ID 非递增而产生性能下降。
- 时间序列数据的查询效率受到影响。
逻辑冲突
部分业务逻辑依赖时间戳作为顺序标识,例如事件排序、数据流时间线等。
怎么解决时钟回拨问题?
直接抛异常
不管 3 * 7 = 21,直接抛出异常,由应用方自行决定处理逻辑。
这种方案简单粗暴,但不是很友好。如果是在一个并发度不高或者业务量不大的系统中,这种粗暴的方式问题不大,但如果是在一个高并发的系统中,这种策略就不是那么友好了。
百度的 Uid-Generator 采取这种方案。
延迟等待
将当前线程阻塞 N 毫秒(N可以根据系统并发程度来决定),之后再重新获取时间,看时间是否比上一次请求的时间大,如果大,说明系统已经恢复正常了,如果还小,则说明系统可能真的出问题了,可以通知相关人员处理。
这种方案实现较为简单,同时能保证生成的 ID 不会重复也是递增的。由于它是采用阻塞等待的方式,所以会阻塞当前线程,如果阻塞时间较长,会影响系统性能。故而,不是很时候高实时性要求的场景。
多节点切换
在分布式系统中,我们不可能只部署一台分布式 ID 生成器。当某个节点检测到时钟回拨时,我们就直接切换到其他正常的节点来生成 ID。
这种方案它减少了单节点因时钟回拨引发的问题,提升了系统的可用性和容错能力,但是它实现较为复杂,同时我们需要额外的维护节点来协调逻辑。
追赶时间
追赶时间也是一种比较暴力的思路。你不是时间慢了么?那我就追赶。首先,不返回 id,然后将 seq 增加(比如 1024 个),然后再次判断是否回拨,如果不是,再增加 1024 个,当 seq 超过了 12 额 maxSeq 时,按照雪花算法的逻辑,时间便会进位,借用下一个时间的 seq,这样就实现了时间的加速。经过若干个加速,则可以实现时间正常。
还有多种其他的方案,比如多时钟雪花算法,时钟调整方案。在面试时,能说出这几点其实也就差不多了。
扩展
百度 UidGenerator
1、RingBuffer 缓存机制
UidGenerator 通过引入 RingBuffer(环形缓冲区)预生成并缓存一批唯一 ID。
- 预生成 ID:在系统运行期间,提前生成一批 ID 并存储在
RingBuffer中。 - 异步填充:当
RingBuffer中的可用 ID 数量低于设定的阈值时,异步线程会填充新的 ID,确保缓冲区始终有足够的 ID 可供分配。 - 无锁设计:通过无锁的
RingBuffer设计,提高并发性能,减少锁竞争带来的性能瓶颈。
这种机制减少了对系统时间的实时依赖,即使发生短暂的时钟回拨,预生成的 ID 也能继续使用,避免了因时间倒退导致的 ID 冲突。
2、WorkerId 分配机制
UidGenerator 在启动时,通过数据库表(如 WORKER_NODE)为每个实例分配唯一的 WorkerId:
- 数据库自增 ID:每个实例启动时,向数据库插入一条记录,获取自增的主键 ID 作为 WorkerId,确保每个实例的 WorkerId 唯一且不重复。
- 实例重启限制:由于 WorkerId 的位数限制,实例的重启次数受到一定限制,需根据业务需求合理配置。
这种机制确保了分布式环境下各实例的 WorkerId 唯一性,避免了因 WorkerId 冲突导致的 ID 重复。
3、时间递增策略
在生成 ID 时,UidGenerator 采用时间递增策略,减少对系统物理时间的依赖:使用逻辑时间(如自增的计数器)来维护时间戳部分,避免直接依赖系统时间。同时,通过控制时间偏移,确保生成的时间戳始终递增,即使系统时间发生回拨,也不会影响 ID 的有序性。
这种策略有效避免了因系统时间回拨导致的 ID 重复或乱序问题。
4、时钟回拨处理
UidGenerator 在生成 ID 时,会检测系统时间的变化。如果检测到当前时间小于上一次生成 ID 的时间,说明发生了时钟回拨。对于检测到的时钟回拨,UidGenerator 选择抛出异常,比较粗暴哈。
美团 Leaf 方案
美团的 Leaf 在应对时钟回拨的问题时,采用如下策略:
1、启动时的时间校验
在 Leaf 的 Snowflake 模式 下,服务启动时会执行以下步骤:
- 检查 ZooKeeper 节点:服务启动时,首先检查自己是否在 ZooKeeper 的
leaf_forever节点下注册过。- 已注册:如果已注册,则获取自身的
workerId,并将当前系统时间与 ZooKeeper 中记录的时间进行比较。如果当前系统时间小于 ZooKeeper 中记录的时间,说明发生了较大的时钟回拨,服务启动失败并报警。 - 未注册:如果未注册,证明是新服务节点,直接创建持久节点并写入自身系统时间。
- 已注册:如果已注册,则获取自身的
- 时间校准:新节点会综合其他 Leaf 节点的系统时间来判断自身系统时间是否准确。具体做法是获取
leaf_temporary下的所有临时节点的服务 IP 和端口,然后通过 RPC 请求得到所有节点的系统时间,计算平均值。 - 时间偏差检测:如果当前系统时间与平均值的差异在可接受范围内,认为当前系统时间准确,正常启动服务,并在
leaf_temporary下创建临时节点维持租约。否则,认为本机系统时间发生了较大偏移,启动失败并报警。
2、运行时的时间监控
在服务运行过程中,Leaf 定期(默认每 3 秒)上报自身系统时间到 ZooKeeper 的 leaf_forever 节点。上报时,如果发现当前时间戳小于上次上报的时间戳,说明发生了时钟回拨,Leaf 会放弃上报,并可能采取进一步措施,如暂停服务或报警。通过这种机制,Leaf 能够在运行时检测到时钟回拨,避免因时间倒退导致的 ID 重复问题。
3、生成 ID 时的时间校验
在生成 ID 的过程中,Leaf 也会进行时间校验:
- 时间回拨检测:如果检测到当前时间小于上次生成 ID 的时间戳,说明发生了时钟回拨。
- 小幅回拨:如果回拨幅度在 5 毫秒以内,Leaf 会等待两倍的回拨时间,然后重新获取当前时间。
- 大幅回拨:如果回拨幅度超过 5 毫秒,Leaf 会抛出异常,并可能触发报警机制。
闰秒问题
闰秒是由于地球自转不完全均匀而增加或减少的时间补充,用来保持协调世界时(UTC)与天文时间的同步。由于地球自转逐渐减缓,UTC和地球自转时间(UT)之间会有差异,因此有时需要在UTC中插入一个额外的秒来保持同步。
- 增加闰秒:当地球自转速度较慢时,会在UTC中加入一个额外的秒。
- 减少闰秒:当地球自转速度较快时,可能会在UTC中减去一个秒。
比如 2017 年就因为闰秒增加了一秒,出现了原本不存在的 7:59:60,这润一秒不知道有多少小伙伴在加班了。

闰秒是具有破坏性的,尤其是那些在依赖精确时间戳的应用服务上,例如依赖雪花算法生成 ID 的服务,很有可能导致其不可用。所以 2022 年 11 月,在第 27 届国际计量大会上,科学家和政府代表投票决定到 2035 年取消闰秒。
更多推荐

所有评论(0)