如何用雪花算法生成分布式唯一 ID

域名注册
域名注册 正式会员超兽战士 👑年卡会员
发布于 2026-10-07 13:19 ·1 浏览 ·0 回复

结论:雪花算法(Snowflake)用 64 位 long 把「毫秒时间戳 + 机器 ID + 序列号」拼成一个本地生成的、全局唯一的、趋势递增的 ID,单机每秒可产出 409.6 万个,不依赖网络和中心服务,是目前分布式 ID 的主流方案。

雪花算法的 64 位是怎么分配的?

结论:1 位符号位 + 41 位时间戳 + 10 位机器 ID + 12 位序列号,共 64 位,正好塞进一个 Java long。

  • 1 位符号位:恒为 0,保证 ID 是正数。
  • 41 位毫秒时间戳:存的是「当前毫秒 − 自定义起始纪元」。2^41 毫秒 ≈ 69.7 年,纪元选 2020-01-01 00:00:00 UTC(1577808000000L)可用到 2089 年。
  • 10 位机器 ID:拆成 5 位数据中心 ID + 5 位工作机器 ID,最多 32 × 32 = 1024 个节点。
  • 12 位序列号:同一毫秒内自增,范围 0~4095,也就是单节点每毫秒最多 4096 个 ID,折合每秒 409.6 万个。

雪花算法怎么实现?

结论:核心是 nextId() 方法里的三段判断——是否时钟回拨、是否同一毫秒、序列号是否溢出。

public class SnowflakeIdGenerator {
    private static final long EPOCH = 1577808000000L;   // 2020-01-01 UTC
    private static final long SEQUENCE_MASK = ~(-1L << 12); // 4095
    private static final long TIMESTAMP_SHIFT = 22L;  // 12+5+5
    private static final long DATACENTER_SHIFT = 17L; // 12+5
    private static final long WORKER_SHIFT = 12L;

    private final long workerId, datacenterId;
    private long sequence = 0L, lastTimestamp = -1L;

    public synchronized long nextId() {
        long ts = System.currentTimeMillis();
        if (ts < lastTimestamp) {                 // 时钟回拨
            long offset = lastTimestamp - ts;
            if (offset <= 5) ts = waitUntil(lastTimestamp); // 5ms 内自旋等待
            else throw new IllegalStateException("时钟回拨 " + offset + "ms");
        }
        if (ts == lastTimestamp) {
            sequence = (sequence + 1) & SEQUENCE_MASK;
            if (sequence == 0) ts = waitUntil(lastTimestamp + 1); // 本毫秒用尽
        } else {
            sequence = 0L;
        }
        lastTimestamp = ts;
        return ((ts - EPOCH) << TIMESTAMP_SHIFT)
             | (datacenterId << DATACENTER_SHIFT)
             | (workerId << WORKER_SHIFT)
             | sequence;
    }
}

nextId() 必须加 synchronized 或改用 AtomicLong,否则并发下序列号会算错。

时钟回拨怎么办?

结论:小回拨等待、大回拨报错是最简单也最稳的策略,回拨超过 5ms 直接抛异常触发告警,不要硬扛。

时钟回拨的常见原因:NTP 校时、虚拟机迁移、容器宿主机时间被手动修改。三种主流处理方式:

  1. 等待:回拨 ≤ 5ms 时自旋到 lastTimestamp 再生成,业务无感知。
  2. 备用 workerId:美团 Leaf 的做法,回拨时切换到另一个机器 ID,避免生成重复 ID。
  3. 拒绝服务:回拨过大直接抛异常,让上层重试或降级。

生产环境建议所有节点开启 NTP 同步,并监控回拨次数。

10 位机器 ID 怎么分配不重复?

结论:容器化环境不要用「IP 后 10 位」或 hostname 哈希,优先用 Kubernetes StatefulSet 的序号(0~1023),非 K8s 环境用 Redis 或 ZooKeeper 注册。

可选方案对比:

  • Redis INCR:启动时 INCR snowflake:worker 取值并对 1024 取模,简单但有回收问题。
  • ZooKeeper 顺序节点:Leaf 用的方案,临时节点 + 顺序号,宕机自动释放。
  • MySQL 自增表:节点启动时 INSERT 一次拿自增 ID,配合心跳表清理。
  • K8s StatefulSet:直接用 Pod 名 app-0、app-1 的数字部分,最省事。

雪花算法和 UUID、数据库自增 ID 有什么区别?

结论:UUID 无序导致 InnoDB 聚簇索引频繁页分裂,数据库自增 ID 有单点和分库分表难题,雪花算法兼顾有序性和去中心化。

方案长度趋势递增依赖
UUID v4128 位(36 字符)否无
MySQL 自增64 位是数据库单点
Redis INCR64 位是Redis
雪花算法64 位近似递增仅需分配 workerId

雪花 ID 在同一节点内严格递增,跨节点只保证按毫秒趋势递增,这对 MySQL 主键索引已经足够友好。

返回给前端要注意什么?

结论:雪花 ID 是 64 位,超过 JavaScript Number.MAX_SAFE_INTEGER(9007199254740991),必须序列化成字符串返回。

Jackson 里加 @JsonSerialize(using = ToStringSerializer.class) 即可,否则前端拿到的 ID 末几位会被截断成 0,出现「两条数据 ID 一样」的诡异 bug。

总结一下:雪花算法的 64 位结构决定了它的上限是单节点 409.6 万 ID/秒、1024 个节点、69.7 年,落地时要解决三件事——nextId() 加锁、时钟回拨策略、workerId 分配,最后别忘了把 ID 以字符串形式吐给前端。

版权声明:本文来自 GJ站长论坛《如何用雪花算法生成分布式唯一 ID》
原文链接:https://www.gj0.com/thread-303.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。

全部回复 0

还没有回复,来抢沙发~