趋势递增唯一 ID:Snowflake 为什么适合分布式发号?

严格单调递增 ID 很难水平扩展,而基于时间戳的趋势递增 ID 放宽了全局顺序要求:只要整体随时间递增,局部允许少量乱序,就可以让多个机房、多个服务实例并行生成唯一 ID。Snowflake 的核心价值,就是把时间、机房、实例和毫秒内序列号编码进一个 64 位整数里。

趋势递增唯一 ID:基于时间戳

一、核心主线

如何利用时间戳生成趋势递增的分布式唯一 ID,并解决多实例、同毫秒并发、Worker ID 分配和时钟回拨问题。

整体思路:

1
2
3
4
5
6
7
8
9
10
11
时间天然递增
↓
不能直接用完整时间戳做 ID
↓
使用相对时间戳节省位数
↓
把机房、实例、并发序列编码进 ID
↓
形成 Snowflake 类算法
↓
再处理 Worker ID 分配和时钟回拨

二、为什么不能直接用时间戳做唯一 ID

时间戳虽然递增,但不能直接作为唯一 ID。

原因是高并发场景下,多个请求可能在同一时间到达:

  • 不同服务实例在同一毫秒生成 ID;
  • 同一服务实例在同一毫秒处理多个请求;
  • 如果 ID 只包含时间戳,就会重复。

所以,基于时间戳的 ID 必须额外区分:

1
2
3
4
时间
+ 机房
+ 服务实例
+ 同一毫秒内的请求序列号

三、正确使用时间戳

1. 使用相对时间戳

64 位 ID 空间有限,不能简单把完整系统时间戳直接塞进去。

更合理的做法是设置一个业务起始时间,例如系统上线时间:

1
相对时间戳 = 当前时间 - 系统起始时间

这样可以节省大量高位空间。

例如:

  • 如果按秒记录,几十年只需要约 31 位;
  • 如果按毫秒记录,约 41 位即可支持长期运行。

2. 时间戳放在高位

整数大小主要由高位决定。

因此,时间戳应放在 ID 的高位:

1
2
3
高位:时间戳
中间:机房 / 实例
低位:毫秒内序列号

这样随着时间推进,生成的 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
2
3
4
5
6
7
8
9
10
11
获取当前毫秒时间戳
↓
读取机房 ID
↓
读取 Worker ID
↓
生成同毫秒内自增序列号
↓
按位左移并拼接
↓
得到 64 位唯一 ID

五、Snowflake 的灵活应用

Snowflake 不是固定模板,而是一种设计思想:

把业务需要区分的变量,按照容量需求分配到 64 位整数的不同比特位中。

例如书中给出的业务条件:

  • 3 个机房;
  • 未来每秒近 10 亿次发号需求;
  • 单个服务实例每秒处理 100 万请求;
  • 期望系统运行 30 年。

可以调整为:

1
2
3
4
5
1位符号位
40位毫秒时间戳
2位机房ID
11位服务实例ID
10位毫秒内并发序列号

这种设计说明:

  • 位数分配要根据业务规模估算;
  • 机房数、实例数、并发数之间可以权衡;
  • 不要机械照搬经典 Snowflake 的 41-5-5-12。

六、Worker ID 分配

1. 为什么需要 Worker ID

多个 ID 生成器实例同时运行时,必须保证每个实例拥有不同的 Worker ID。

否则两个实例在同一毫秒内可能生成完全相同的 ID。

2. 基于数据库分配

可以创建 worker_id 表,用自增主键分配 Worker ID:

1
2
3
4
5
6
7
实例启动
↓
携带本机 IP 查询 worker_id 表
↓
如果已存在,使用已有主键
↓
如果不存在,插入 IP 并使用新主键

优点是实现简单,适合规模不大的场景。

3. 基于 etcd 分配

etcd 提供全局递增的 Revision 机制。

分配流程:

1
2
3
4
5
6
7
8
9
实例启动
↓
创建 worker_id_{IP} 键
↓
读取所有 worker_id_ 前缀键
↓
按 create_revision 排序
↓
当前实例所在下标 = Worker ID

这种方案更适合分布式协调场景,也可用 ZooKeeper、etcd 等组件实现。


七、时钟回拨问题

1. 问题来源

Snowflake 依赖机器时间。

机器时间可能因为 NTP 校准发生回拨:

1
2
3
当前机器时间:11:05:05
↓ NTP 校准
校准后时间:11:05:00

如果服务继续发号,就可能生成与 5 秒前相同时间段内的 ID,导致重复。

2. 解决思路

每次生成 ID 时,都要比较:

1
当前毫秒时间戳 vs 上一次发号毫秒时间戳

如果当前时间小于上一次时间,说明发生了时钟回拨。

处理方式:

回拨情况 处理策略
回拨时间较短 阻塞等待一小段时间后重试
回拨时间较长 直接拒绝请求,避免重复 ID
机器长期漂移严重 从服务集群中摘除该实例

也可以对 ID 生成器集群关闭 NTP 自动同步,避免运行中突然回拨,但需要额外监控时钟漂移。


八、最终架构

基于 Snowflake 的唯一 ID 生成器最终架构如下:

1
2
3
4
5
6
7
8
9
10
多个机房
├── 每个机房有独立机房编号
├── 每个机房部署 Worker ID 分配器
└── 每个机房部署多个 ID 生成器实例
↓
实例启动时获取 Worker ID
↓
本地维护毫秒时间戳和毫秒内序列号
↓
结合机房 ID 生成唯一 ID

这种架构的特点:

  • 多机房可并行发号;
  • 多实例可水平扩展;
  • 发号过程主要在本地完成;
  • 性能高,不依赖中心服务逐次分配;
  • 生成结果趋势递增,适合作为数据库主键。

九、核心架构思想

  1. 趋势递增比严格单调递增更适合分布式高并发系统。
  2. 时间戳适合放在 ID 高位,用来保证整体递增趋势。
  3. 不能直接用时间戳做唯一 ID,必须加入实例和并发序列信息。
  4. Snowflake 的本质是按位编码,把多个变量压缩进 64 位整数。
  5. 位数分配应根据业务容量灵活调整,而不是死套经典模板。
  6. Worker ID 必须全局唯一,否则多实例会生成重复 ID。
  7. Worker ID 可以通过数据库自增主键、etcd 或 ZooKeeper 分配。
  8. 同一毫秒内的序列号决定单实例瞬时发号能力。
  9. 时钟回拨是基于时间戳发号的核心风险,必须检测并处理。
  10. Snowflake 用局部有序换取高并发、高可用和水平扩展能力。

十、一句话总结

基于时间戳的趋势递增唯一 ID 通过 Snowflake 类算法,把相对时间戳、机房 ID、Worker ID 和毫秒内序列号编码进 64 位整数,从而实现多机房、多实例的高并发本地发号;它牺牲严格全局单调递增,换来了更好的分布式扩展能力,但必须解决 Worker ID 分配和时钟回拨问题。

© 2026 DadaVinCi's Blog

Elegant theme by Shiro · Made by Acris with ❤️