分布式 ID 生成方案:雪花算法与号段模式
FreeGuideOnline
最新
2026-06-16
分布式 ID 的核心问题
在单体应用中,数据库自增主键就能轻松生成唯一 ID。但系统一旦拆分到多个数据库或微服务,就必须在分布式环境下保证 ID 的全局唯一性、高性能、高可用,同时尽量满足有序性、信息安全等要求。
分布式 ID 通常需要解决以下矛盾:
- 唯一性:不能出现重复 ID。
- 趋势递增:对 MySQL InnoDB 等存储引擎友好,减少页分裂。
- 高性能:生成速度要快,不能成为系统瓶颈。
- 高可用:避免单点故障导致 ID 生成服务瘫痪。
- 信息脱敏:连续的 ID 容易被遍历,希望 ID 中不直接暴露业务量。
目前业界最主流的两种方案,就是 雪花算法 (Snowflake) 和 号段模式 (Segment)。
雪花算法 (Snowflake)
Twitter 开源的 Snowflake 算法,用一个 64 位的长整型数字作为全局唯一 ID。它的思想是将不同信息放到不同的比特位,由机器自身产生 ID,不依赖数据库等中心节点,性能极高。
Snowflake 的 64 位结构
一个经典的划分如下(不同实现可以调整各段位数):
| 段 | 位数 | 说明 |
|---|---|---|
| 符号位 | 1 bit | 固定为 0,表示正数 |
| 时间戳 | 41 bits | 毫秒级时间戳,从自定义起始时间开始的差值 |
| 机器 ID | 10 bits | 可以分成 5 位数据中心 + 5 位工作机器,共 1024 个节点 |
| 序列号 | 12 bits | 同一毫秒内递增的序号,每毫秒最多 4096 个 ID |
- 时间戳部分:选择 41 位毫秒时间戳,可用 69 年 (2^41 / 1000 / 3600 / 24 / 365 ≈ 69.7)。
- 机器 ID:需要手动为每个实例分配唯一的机器号和/dataCenterId,确保全局不重复。
- 序列号:同一毫秒内并发请求,序列号从 0 自增。超过 4096 则等待下一毫秒。
工作原理
- 根据当前时间与自定义起始时间计算出毫秒级的时间差,放入 41 bits。
- 将当前机器的 dataCenterId 和 workerId 拼接到指定位。
- 检查本机当前毫秒内是否已经生成过 ID:
- 如果是当前毫秒内的第一个请求,sequence 置 0。
- 如果本毫秒内已有请求,sequence 自增 1。
- 把这些段移位拼接成一个 long 型数值返回。
若发生时钟回拨,sequence 递增方案会退化为等待或抛异常。这是 Snowflake 最大的坑,需要专门处理。
时钟回拨问题及解决方案
时钟回拨可能由 NTP 校时、虚拟机迁移等原因引起。一旦发生,可能产生重复 ID。常见对策:
- 容忍小范围回拨:记录最后生成的时间戳和 sequence,如果回拨差值小于一定阈值(如 5ms),在内存中等待到时间追上。
- 备用 workerId:发现时钟回拨后,临时切换到一个备用 workerId 生成 ID,当前 workerId 暂停服务。
- 扩展位:部分实现(如百度的 UidGenerator、美团的 Leaf)将时间戳位扩展成秒级,并利用 ring buffer 等方式提前生成,降低对时钟的敏感度。
- 拒绝服务直到恢复:直接抛出异常,让调用方重试,依赖运维尽快修正时钟。
优点与缺点
✅ 优点:
- 纯内存计算,性能极高,单机 QPS 可达数十万。
- ID 整体趋势递增,对数据库索引友好。
- 不依赖数据库,去中心化,扩展性好。
❌ 缺点:
- 强依赖机器时钟,时钟回拨可能产生重复 ID。
- workerId 需要手动分配或借助协调服务(如 ZooKeeper),增加运维复杂度。
- ID 中包含时间戳信息,虽然安全性比连续自增好,但仍可能暴露数据产生的相对时间。
号段模式 (Segment)
号段模式是一种基于数据库的分布式 ID 方案。它不会每次生成 ID 都去访问数据库,而是一次从数据库取一段 ID 区间(segment)缓存到本地,用完了再去取新的号段。
核心原理
- 在数据库中维护一张发号表,例如
id_alloc,结构如下:CREATE TABLE id_alloc ( biz_tag VARCHAR(128) NOT NULL PRIMARY KEY, -- 业务标识 max_id BIGINT NOT NULL, -- 当前已分配的最大 ID step INT NOT NULL, -- 每次分配的号段长度 update_time TIMESTAMP ); - 每个业务线用唯一的
biz_tag区分。 - 服务启动后,向数据库申请号段:
这条 SQL 是原子操作,利用数据库行锁保证并发更新安全。UPDATE id_alloc SET max_id = max_id + step WHERE biz_tag = 'order' RETURNING max_id - step AS start_id; - 服务拿到
[start_id, start_id + step)这个区间,缓存到本地内存。 - 请求 ID 时直接从本地号段中
AtomaticLong自增获取,号段快用完时,异步或同步地提前去数据库加载下一个号段,避免阻塞。 - 如果服务重启,内存中的未用完 ID 会被丢弃,但不会产生重复(因为数据库 max_id 已经前移)。
这就是双 buffer 优化的精髓:当前号段消耗到某一阈值时,后台线程提前拉取下一个号段,无缝切换。
优缺点
✅ 优点:
- ID 是纯数字,严格递增,适合需要顺序 ID 的业务。
- 不强依赖时钟,没有时钟回拨问题。
- 数据库依赖低,即使数据库短暂不可用,本地缓存的号段还能撑一段时间。
- 实现相对简单,不依赖额外组件,只需一个关系数据库。
❌ 缺点:
- ID 连续性较差:服务重启会丢弃未用完的号段,导致 ID 出现空洞。
- 仍依赖数据库作为中心节点,需要考虑数据库的高可用(主从、多活)。
- 号段存在交叉使用的风险:多实例如果号段分配步长不协调,可能出现 ID 交叉。
两种方案对比与选型
| 维度 | 雪花算法 | 号段模式 |
|---|---|---|
| 性能 | 极高,纯内存计算 | 高,本地缓存号段,定期访问数据库 |
| 唯一性保障 | 依赖 workerId 唯一 + 时钟 | 依赖数据库行锁保证号段不重叠 |
| 趋势递增 | 整体趋势递增,时间局部无序 | 严格递增 |
| 时钟依赖 | 强依赖,时钟回拨需特殊处理 | 无直接依赖 |
| 高可用 | 可无中心化部署,但 workerId 分配需协调 | 数据依赖数据库,需保证 DB 高可用 |
| 运维复杂度 | 需要维护 workerId 分配,处理时钟回拨 | 相对简单,依赖 DB |
| 适用场景 | 高性能、去中心化、可接受趋势递增 | 强顺序要求、不希望暴露时间信息、运维简单的场景 |
实际落地建议
- 雪花算法常用于微服务架构,结合容器编排时,可以通过启动参数传入 workerId,或基于 IP、主机名等自动生成唯一机器标识。
- 号段模式常被用在订单号、交易流水号等强顺序依赖的场景。美团的 Leaf 框架就同时实现了这两种模式,可以根据业务标签选择。
- 如果对数据安全要求极高,还可以在号段模式生成的纯数字 ID 基础上进行加密或混淆,变成无规律的字符串。
- 无论哪种方案,都应提前评估业务量,计算出 ID 的上限寿命(特别是雪花算法的时间戳位数),避免未来溢出。
总结
分布式 ID 生成没有银弹,需要根据业务量、顺序需求、运维能力来选择。
- 追求极高性能、去中心化、能处理时钟风险的团队,首选雪花算法。
- 期望强顺序、数据脱敏、运维简单的团队,号段模式更适用。
两种方案也可以组合使用,例如利用号段模式生成一个有序的“基底 ID”,再用雪花算法的时间戳和序列填充其他部分,以求性能与顺序的平衡。理解其核心原理后,可以根据自己的场景灵活改造。