号段模式的天花板
双 buffer 让号段模式稳了下来,老王却盯上了新的压力源:公司要做 IoT 设备埋点,每秒百万级事件要打 ID;发号服务再稳,也是所有业务的汇聚点——只要发号动作还要走到某个中心节点,中心就是天花板。Twitter 十几年前遇到了同样的问题,给出的答案是雪花算法(Snowflake):不依赖任何中心,每个节点自己发号。
64 位的三段式拆解
一个 long 型 64 位,被切成四段:
0 | 000110... (41 bit 毫秒时间戳)| 0000000001 (10 bit 机器号)| 000000000001 (12 bit 序列号)
符号位恒 0 保证正数,真正干活的是后三段1 bit 符号位:恒为 0,保证生成的 ID 是正数,Java 的 long 无符号判断不踩坑。
41 bit 时间戳:存「当前毫秒减去起算纪元」的偏移量。2 的 41 次方毫秒约 69 年——选好起算时间(比如项目上线日),够用到退休。时间戳在最高位,意味着 ID 整体随时间单调递增,InnoDB 顺序写入的梦想实现了。
10 bit 机器号:2 的 10 次方 1024 个节点。每个节点持有一个全局不重复的编号,同一毫秒里不同节点生成的 ID 天然错开——机器号就是分布式下的「身份证」,它的分配问题单独留到第 8 篇。
12 bit 序列号:同一毫秒、同一节点内区分不同 ID 用,2 的 12 次方 4096 个。单机单毫秒发满 4096 个后,怎么办?自旋等待到下一毫秒重新计数。算总账:每毫秒 4096 个乘以 1000 毫秒,单机理论上限约每秒 409 万——实测百万级轻轻松松,百万埋点需求直接击穿。
生成流程:一次自旋的故事
核心逻辑十几行伪代码就能说清:
synchronized long nextId() {
long now = System.currentTimeMillis();
if (now 为上一毫秒) {
seq = (seq + 1) 与上 4095; // 12 bit 内自增
if (seq 归零) { // 本毫秒额度用尽
now = 等待下一毫秒(); // 自旋,不忙等死循环
}
} else {
seq = 0; // 新毫秒从头计数
}
lastTimestamp = now;
return (now - 纪元) 左移 22 | 机器号 左移 12 | seq;
}三维唯一性一目了然:不同时间(41 bit)、不同机器(10 bit)、不同序列(12 bit),任意一维不同,ID 就不同——世界没有两片相同的雪花,分布式里没有两个相同的 ID。位运算拼装零锁竞争(方法级同步只锁本节点),纳秒级完成。
位数不是圣旨
41、10、12 是 Twitter 按自己场景切的,不是标准。位数是道分配题:时间给得多,可用年头长;机器给得多,横向扩展强;序列给得多,单机峰值高。实际落地时按业务重新分配很常见——机器只有几十台却要发三十年,就可以从机器号里匀几位给时间戳;反过来集群上万台,就从时间里匀给机器号。分配定终身,上线前想清楚,改位数的代价等于换号段(老 ID 还要兼容)。
两个隐藏前提
雪花把发号变成了本机时钟的操作,代价是两个强前提:第一,机器时钟必须可信——时间戳来自系统时钟,时钟回拨会让它发出「过去」的号,这是整个算法的死穴,第 6 篇专题;第二,机器号必须全局唯一——两台机器拿到同一个机器号,同一毫秒就可能发出相同 ID,唯一性瞬间崩塌,第 8 篇专题。
小结
雪花算法一句话:用 64 位的空间换来了时间、机器、序列三维唯一,本地发号吞吐封顶、趋势递增养索引。它把「中心发号」变成「人人发号」,也把风险从「中心挂了」变成「时钟飘了、机器号重了」。下一篇,正面刚它的死穴:时钟回拨到底怎么发生,又有多致命。
评论 (0)