雪花算法(Snowflake ID)原理:64 位怎么拆、时钟回拨怎么办

· 约 3 分钟 ❄️ Snowflake ID 解析

订单号、消息 ID、用户 ID——这些一长串数字背后,很多用的是雪花算法(Snowflake)。它最早由 Twitter 开源,用一个 64 位整数同时解决了”分布式唯一”和”大致有序”两个难题。理解它的位结构和那个最容易翻车的时钟回拨问题,才能用好、也能看懂别人的 ID。

为什么需要雪花 ID

先看两个常见方案的短板:

  • 数据库自增主键:依赖单点发号,分库分表后无法全局唯一,高并发下是瓶颈,ID 还会泄露业务量。
  • UUID:能本地生成、全局唯一,但完全无序,做数据库主键会让 B+ 树频繁页分裂、写入变慢。

雪花 ID 取两者之长:本地生成、全局唯一、又大致按时间递增,对索引友好。

64 位怎么拆

经典 Twitter 布局,从高位到低位:

位数作用
符号位1固定 0,保证是正数
时间戳41毫秒级,相对”起始纪元”的偏移,约够 69 年
机器位10常拆成 5 位数据中心 + 5 位机器,支持 1024 节点
序列号12同一毫秒内自增,每节点每毫秒最多 4096 个

把这四段拼成一个 64 位整数,就是一个雪花 ID。

为什么”大致有序”

因为时间戳在高位。时间靠前生成的 ID,高位更小,所以整体趋势是递增的——这正是它对数据库索引友好的原因(顺序写入,少页分裂)。

但要注意:同一毫秒内、不同机器之间的先后由机器位和序列决定,不代表真实生成时刻的严格顺序。所以雪花 ID 是”趋势递增”,不能当成精确排序的依据。

起始纪元(epoch)的意义

41 位毫秒时间戳如果从 1970 年(Unix 纪元)算起,很快就会逼近上限。各家于是设一个自定义起始纪元——比如 Twitter 用 2010-11-04——时间戳只记”距离这个纪元过了多少毫秒”,把 69 年的可用窗口推到上线之后。这也是为什么解析时必须选对纪元,否则时间全错。

时钟回拨:最致命的坑

雪花 ID 的唯一性建立在一个假设上:时间只会前进

可现实里,服务器时间会被 NTP 校正、被运维手动调整。一旦时间往回拨,就可能生成和过去重复的时间戳,进而产生重复 ID——这对一个”唯一 ID 生成器”是灾难。

生产级发号器必须处理回拨,常见策略:

  1. 拒绝发号:检测到当前时间 < 上次时间,直接抛错并告警,等时间追平。
  2. 等待追平:阻塞到时钟追上上次记录的时间再发。
  3. 借位容忍:用扩展位或备用序列吸收小幅回拨。

自研发号器上线前,务必拿真实 ID 验证位布局,并明确回拨策略。

各平台布局不一样

平台起始纪元时间戳机器/其他序列
Twitter / 通用2010-11-0441 bit5+5 bit12 bit
Discord2015-01-0142 bit5+5 bit12 bit
MastodonUnix 纪元48 bit16 bit
Sonyflake2014-09-0139 bit(×10ms)16 bit8 bit

解析时选错预设,时间和机器位就全错——时间明显不合理(1970 年或几百年后),先怀疑纪元/位布局选错了

两个实用技巧

  • 按时间分页:某时刻对应的”最小 ID”可由 (t − 纪元) 左移机器位与序列位推出,用 id > 最小ID 就能查该时刻之后的数据,省掉 created_at 索引(Discord 的游标分页即如此)。
  • 前端解析用 BigInt:64 位超过 JS Number 的安全整数范围(2^53),直接当数字会丢精度,必须用 BigInt 或字符串处理。

看懂了位结构和纪元,一串数字 ID 里藏着的生成时间、机器、序列就一目了然了。

❓ 常见问题

为什么不用数据库自增主键,要搞雪花 ID?

自增主键依赖单点数据库发号,分库分表后无法保证全局唯一,且高并发下发号会成为瓶颈、ID 还会暴露业务量。UUID 虽然能本地生成、全局唯一,但完全无序,做数据库主键会导致 B+ 树频繁页分裂、写入性能差。雪花 ID 兼顾两者:本地生成、全局唯一、又大致按时间递增,对索引友好。

64 位的雪花 ID 具体怎么分配?

经典 Twitter 布局:最高 1 位固定为 0(保证是正数),接着 41 位毫秒时间戳,再 10 位机器标识(常拆成 5 位数据中心 + 5 位机器),最后 12 位同毫秒内自增序列号。41 位毫秒约够用 69 年,10 位支持 1024 个节点,12 位序列让每个节点每毫秒最多发 4096 个 ID。

雪花 ID 为什么是"大致有序"而不是严格有序?

因为高位是时间戳,时间靠前的 ID 整体更小,所以全局趋势递增、对数据库索引友好。但同一毫秒内、不同机器之间的先后由机器位和序列决定,不代表真实生成时刻的严格顺序;跨节点也无法保证两个相邻时刻的 ID 严格单调。所以是"趋势递增",不能当成精确排序依据。

时钟回拨是什么?为什么对雪花 ID 是致命问题?

雪花 ID 的唯一性建立在"时间只会前进"的假设上。一旦服务器时间被 NTP 校正或手动调整往回拨,就可能生成和过去重复的时间戳,进而产生重复 ID。常见应对:检测到回拨时拒绝发号并报警、或等待到追平上次时间再发、或用扩展位(如借用序列/备用位)容忍小幅回拨。生产级发号器必须处理这一点。

为什么不同平台解析同一串数字结果不同?

因为各家的起始纪元和位分配都不同。Twitter 纪元是 2010-11-04、Discord 是 2015-01-01、Mastodon 直接用 Unix 纪元;时间戳/机器/序列各占多少位也不一致。解析时选错预设,算出的时间和机器位就是错的——如果时间明显不合理(1970 年或几百年后),多半是纪元/位布局选错了。

怎么用雪花 ID 做按时间分页?

因为 ID 趋势递增,某个时刻 t 对应的"最小 ID"≈ (t - 纪元) 左移机器位与序列位之后的值。要查 t 之后的数据,用 id > 该最小 ID 即可,无需额外的 created_at 索引。Discord 的 before/after 游标分页就是这么做的。注意这是基于时间戳推算的边界,毫秒内的顺序不精确。

前端 JavaScript 解析雪花 ID 要注意什么?

雪花 ID 是 64 位整数,超过 JavaScript Number 能精确表示的范围(2^53)。直接当普通数字解析会丢精度、算错时间戳和序列号。必须用 BigInt 做位运算,或把 ID 当字符串处理,才能保证每一位都准确。

❄️ 打开 Snowflake ID 解析 反解雪花 ID 时间戳/机器位/序列号·Twitter/Discord/Mastodon/Sonyflake 预设+自定义·二进制可视化·本地运行