回答
由于雪花算法高度依赖系统时钟,当时钟回拨发生时,基于时间戳的分布式 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,这样就实现了时间的加速。经过若干个加速,则可以实现时间正常。
还有多种其他的方案,比如多时钟雪花算法,时钟调整方案。在面试时,能说出这几点其实也就差不多了,大明哥在网上找了几篇介绍其他方案的文章,各位小伙伴可以研究研究:
- https://blog.hackerpie.com/posts/algorithms/snowflake/multiple-clocks-snowflake/
- https://cloud.tencent.com/developer/news/678423
扩展
这里介绍下百度的 UidGenerator 和美团 Leaf 项目,以下内容摘自:https://chinalhr.github.io/post/uid-generator-scheme/
百度 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 年取消闰秒。
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。