分布式 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 则等待下一毫秒。

工作原理

  1. 根据当前时间与自定义起始时间计算出毫秒级的时间差,放入 41 bits。
  2. 将当前机器的 dataCenterId 和 workerId 拼接到指定位。
  3. 检查本机当前毫秒内是否已经生成过 ID:
    • 如果是当前毫秒内的第一个请求,sequence 置 0。
    • 如果本毫秒内已有请求,sequence 自增 1。
  4. 把这些段移位拼接成一个 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)缓存到本地,用完了再去取新的号段。

核心原理

  1. 在数据库中维护一张发号表,例如 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
    );
    
  2. 每个业务线用唯一的 biz_tag 区分。
  3. 服务启动后,向数据库申请号段:
    UPDATE id_alloc 
    SET max_id = max_id + step 
    WHERE biz_tag = 'order' 
    RETURNING max_id - step AS start_id;
    
    这条 SQL 是原子操作,利用数据库行锁保证并发更新安全。
  4. 服务拿到 [start_id, start_id + step) 这个区间,缓存到本地内存。
  5. 请求 ID 时直接从本地号段中 AtomaticLong 自增获取,号段快用完时,异步或同步地提前去数据库加载下一个号段,避免阻塞。
  6. 如果服务重启,内存中的未用完 ID 会被丢弃,但不会产生重复(因为数据库 max_id 已经前移)。

这就是双 buffer 优化的精髓:当前号段消耗到某一阈值时,后台线程提前拉取下一个号段,无缝切换。

优缺点

✅ 优点:

  • ID 是纯数字,严格递增,适合需要顺序 ID 的业务。
  • 不强依赖时钟,没有时钟回拨问题。
  • 数据库依赖低,即使数据库短暂不可用,本地缓存的号段还能撑一段时间。
  • 实现相对简单,不依赖额外组件,只需一个关系数据库。

❌ 缺点:

  • ID 连续性较差:服务重启会丢弃未用完的号段,导致 ID 出现空洞。
  • 仍依赖数据库作为中心节点,需要考虑数据库的高可用(主从、多活)。
  • 号段存在交叉使用的风险:多实例如果号段分配步长不协调,可能出现 ID 交叉。

两种方案对比与选型

维度 雪花算法 号段模式
性能 极高,纯内存计算 高,本地缓存号段,定期访问数据库
唯一性保障 依赖 workerId 唯一 + 时钟 依赖数据库行锁保证号段不重叠
趋势递增 整体趋势递增,时间局部无序 严格递增
时钟依赖 强依赖,时钟回拨需特殊处理 无直接依赖
高可用 可无中心化部署,但 workerId 分配需协调 数据依赖数据库,需保证 DB 高可用
运维复杂度 需要维护 workerId 分配,处理时钟回拨 相对简单,依赖 DB
适用场景 高性能、去中心化、可接受趋势递增 强顺序要求、不希望暴露时间信息、运维简单的场景

实际落地建议

  • 雪花算法常用于微服务架构,结合容器编排时,可以通过启动参数传入 workerId,或基于 IP、主机名等自动生成唯一机器标识。
  • 号段模式常被用在订单号、交易流水号等强顺序依赖的场景。美团的 Leaf 框架就同时实现了这两种模式,可以根据业务标签选择。
  • 如果对数据安全要求极高,还可以在号段模式生成的纯数字 ID 基础上进行加密或混淆,变成无规律的字符串。
  • 无论哪种方案,都应提前评估业务量,计算出 ID 的上限寿命(特别是雪花算法的时间戳位数),避免未来溢出。

总结

分布式 ID 生成没有银弹,需要根据业务量、顺序需求、运维能力来选择。

  • 追求极高性能、去中心化、能处理时钟风险的团队,首选雪花算法
  • 期望强顺序、数据脱敏、运维简单的团队,号段模式更适用。

两种方案也可以组合使用,例如利用号段模式生成一个有序的“基底 ID”,再用雪花算法的时间戳和序列填充其他部分,以求性能与顺序的平衡。理解其核心原理后,可以根据自己的场景灵活改造。