趋势递增唯一 ID:Snowflake 为什么适合分布式发号?
严格单调递增 ID 很难水平扩展,而基于时间戳的趋势递增 ID 放宽了全局顺序要求:只要整体随时间递增,局部允许少量乱序,就可以让多个机房、多个服务实例并行生成唯一 ID。Snowflake 的核心价值,就是把时间、机房、实例和毫秒内序列号编码进一个 64 位整数里。
趋势递增唯一 ID:基于时间戳
一、核心主线
如何利用时间戳生成趋势递增的分布式唯一 ID,并解决多实例、同毫秒并发、Worker ID 分配和时钟回拨问题。
整体思路:
1 | 时间天然递增 |
二、为什么不能直接用时间戳做唯一 ID
时间戳虽然递增,但不能直接作为唯一 ID。
原因是高并发场景下,多个请求可能在同一时间到达:
- 不同服务实例在同一毫秒生成 ID;
- 同一服务实例在同一毫秒处理多个请求;
- 如果 ID 只包含时间戳,就会重复。
所以,基于时间戳的 ID 必须额外区分:
1 | 时间 |
三、正确使用时间戳
1. 使用相对时间戳
64 位 ID 空间有限,不能简单把完整系统时间戳直接塞进去。
更合理的做法是设置一个业务起始时间,例如系统上线时间:
1 | 相对时间戳 = 当前时间 - 系统起始时间 |
这样可以节省大量高位空间。
例如:
- 如果按秒记录,几十年只需要约 31 位;
- 如果按毫秒记录,约 41 位即可支持长期运行。
2. 时间戳放在高位
整数大小主要由高位决定。
因此,时间戳应放在 ID 的高位:
1 | 高位:时间戳 |
这样随着时间推进,生成的 ID 整体也会越来越大,满足趋势递增。
四、Snowflake 算法原理
Snowflake 算法将多个分布式变量按位拼接成一个 64 位 long 类型整数。
经典结构如下:
1 | 0 | 41位时间戳 | 5位机房ID | 5位机器ID | 12位序列号 |
各部分含义:
| 位段 | 作用 | 能力 |
|---|---|---|
| 1 位符号位 | 固定为 0,保证正数 | 生成正整数 ID |
| 41 位时间戳 | 当前时间与起始时间的毫秒差 | 可运行约 69 年 |
| 5 位机房 ID | 区分不同机房 | 最多 32 个机房 |
| 5 位机器 ID | 区分服务实例 | 每机房最多 32 个实例 |
| 12 位序列号 | 区分同一毫秒内请求 | 每实例每毫秒最多 4096 个 ID |
生成过程可以理解为:
1 | 获取当前毫秒时间戳 |
五、Snowflake 的灵活应用
Snowflake 不是固定模板,而是一种设计思想:
把业务需要区分的变量,按照容量需求分配到 64 位整数的不同比特位中。
例如书中给出的业务条件:
- 3 个机房;
- 未来每秒近 10 亿次发号需求;
- 单个服务实例每秒处理 100 万请求;
- 期望系统运行 30 年。
可以调整为:
1 | 1位符号位 |
这种设计说明:
- 位数分配要根据业务规模估算;
- 机房数、实例数、并发数之间可以权衡;
- 不要机械照搬经典 Snowflake 的 41-5-5-12。
六、Worker ID 分配
1. 为什么需要 Worker ID
多个 ID 生成器实例同时运行时,必须保证每个实例拥有不同的 Worker ID。
否则两个实例在同一毫秒内可能生成完全相同的 ID。
2. 基于数据库分配
可以创建 worker_id 表,用自增主键分配 Worker ID:
1 | 实例启动 |
优点是实现简单,适合规模不大的场景。
3. 基于 etcd 分配
etcd 提供全局递增的 Revision 机制。
分配流程:
1 | 实例启动 |
这种方案更适合分布式协调场景,也可用 ZooKeeper、etcd 等组件实现。
七、时钟回拨问题
1. 问题来源
Snowflake 依赖机器时间。
机器时间可能因为 NTP 校准发生回拨:
1 | 当前机器时间:11:05:05 |
如果服务继续发号,就可能生成与 5 秒前相同时间段内的 ID,导致重复。
2. 解决思路
每次生成 ID 时,都要比较:
1 | 当前毫秒时间戳 vs 上一次发号毫秒时间戳 |
如果当前时间小于上一次时间,说明发生了时钟回拨。
处理方式:
| 回拨情况 | 处理策略 |
|---|---|
| 回拨时间较短 | 阻塞等待一小段时间后重试 |
| 回拨时间较长 | 直接拒绝请求,避免重复 ID |
| 机器长期漂移严重 | 从服务集群中摘除该实例 |
也可以对 ID 生成器集群关闭 NTP 自动同步,避免运行中突然回拨,但需要额外监控时钟漂移。
八、最终架构
基于 Snowflake 的唯一 ID 生成器最终架构如下:
1 | 多个机房 |
这种架构的特点:
- 多机房可并行发号;
- 多实例可水平扩展;
- 发号过程主要在本地完成;
- 性能高,不依赖中心服务逐次分配;
- 生成结果趋势递增,适合作为数据库主键。
九、核心架构思想
- 趋势递增比严格单调递增更适合分布式高并发系统。
- 时间戳适合放在 ID 高位,用来保证整体递增趋势。
- 不能直接用时间戳做唯一 ID,必须加入实例和并发序列信息。
- Snowflake 的本质是按位编码,把多个变量压缩进 64 位整数。
- 位数分配应根据业务容量灵活调整,而不是死套经典模板。
- Worker ID 必须全局唯一,否则多实例会生成重复 ID。
- Worker ID 可以通过数据库自增主键、etcd 或 ZooKeeper 分配。
- 同一毫秒内的序列号决定单实例瞬时发号能力。
- 时钟回拨是基于时间戳发号的核心风险,必须检测并处理。
- Snowflake 用局部有序换取高并发、高可用和水平扩展能力。
十、一句话总结
基于时间戳的趋势递增唯一 ID 通过 Snowflake 类算法,把相对时间戳、机房 ID、Worker ID 和毫秒内序列号编码进 64 位整数,从而实现多机房、多实例的高并发本地发号;它牺牲严格全局单调递增,换来了更好的分布式扩展能力,但必须解决 Worker ID 分配和时钟回拨问题。