分布式 ID 生成方案总结

目录

一、常见方案

[1. UUID](#1. UUID)

[2. 数据库号段模式](#2. 数据库号段模式)

[3. Redis 自增](#3. Redis 自增)

[4. 雪花算法(Snowflake)](#4. 雪花算法(Snowflake))

二、雪花算法原理

[ID 结构](#ID 结构)

生成过程

完整实现代码

三、雪花算法的优缺点

优点

缺点

[缺点 1:时钟回拨](#缺点 1:时钟回拨)

[缺点 2:机器 ID 需要提前分配](#缺点 2:机器 ID 需要提前分配)

[缺点 3:同一毫秒内不严格有序](#缺点 3:同一毫秒内不严格有序)

四、开源实现推荐

五、怎么选?


做分布式系统绕不开一个问题:怎么生成全局唯一 ID?

单机时代直接用数据库自增主键,分库分表之后就不行了------两个库各自从 1 开始自增,ID 必然冲突。这篇文章聊聊常见的几种分布式 ID 方案,以及目前用得最广的雪花算法的原理和坑。


一、常见方案

1. UUID

java 复制代码
String id = UUID.randomUUID().toString();
// 550e8400-e29b-41d4-a716-446655440000

生成简单,但有两个硬伤:

第一,36 位字符串,占存储空间大;

第二,完全随机无序,用作数据库主键时,每次插入都可能落在 B+ 树的随机位置,导致页频繁分裂,写入性能很差。

适合做日志追踪 ID,不适合做数据库主键。


2. 数据库号段模式

思路:不每次都访问数据库,而是批量取一段 ID,用完了再去取。

sql 复制代码
CREATE TABLE id_generator (
    biz_type  VARCHAR(64) NOT NULL,   -- 业务类型,比如 'order'、'user'
    max_id    BIGINT      NOT NULL,   -- 当前已分配出去的最大 ID
    step      INT         NOT NULL,   -- 每次取多少个(步长)
    version   INT         NOT NULL,   -- 乐观锁,防并发
    PRIMARY KEY (biz_type)
);

每次取号段,执行一次 UPDATE:

sql 复制代码
-- 取 step 个 ID,乐观锁保证并发安全
UPDATE id_generator
   SET max_id  = max_id + step,
       version = version + 1
 WHERE biz_type = 'order'
   AND version  = #{version};

执行成功后,应用拿到 [max_id - step + 1, max_id] 这段 ID 在本地用,用完再取下一段。

优点: ID 有序,可读性好,正常情况下不依赖数据库。
缺点: 依赖数据库,需要做双 buffer(快用完时提前加载下一段),实现稍复杂。

美团开源的 Leaf 就是这个方案的生产级实现。


3. Redis 自增

java 复制代码
// INCR 是原子操作,天然无并发问题
Long id = redisTemplate.opsForValue().increment("order:id");

优点: 性能高,有序。
缺点: 依赖 Redis;必须开启 AOF 持久化,否则 Redis 重启后可能从旧值开始,导致 ID 重复。


4. 雪花算法(Snowflake)

Twitter 开源的方案,纯本地生成,不依赖任何外部组件,性能极高。目前用得最多,下面重点讲。


二、雪花算法原理

ID 结构

雪花算法把一个 64 位的 long 整数划分成 4 段:

复制代码
┌─────┬──────────────────────────────────────────┬──────────┬──────────┬──────────────┐
│  0  │             时间戳(41 位)               │ 数据中心 │  机器ID  │    序列号    │
│ 1位 │         (当前毫秒 - 起始毫秒)             │  (5 位)  │  (5 位)  │   (12 位)   │
└─────┴──────────────────────────────────────────┴──────────┴──────────┴──────────────┘

每段的含义:

位数 说明
符号位 1 位 固定为 0,保证 ID 是正数
时间戳 41 位 当前毫秒 - 自定义起始时间,可用约 69 年
数据中心 ID 5 位 最多支持 32 个数据中心
机器 ID 5 位 每个数据中心最多 32 台机器
序列号 12 位 同一毫秒内自增,最多 4096

单机性能上限: 4096 个/毫秒 = 400 万+/秒,完全够用。


生成过程

每次调用 nextId(),执行以下逻辑:

复制代码
获取当前毫秒时间戳
        ↓
与上次时间戳比较
        ↓
  ┌─────┴──────────────────────────┐
  │                                │
同一毫秒                        新的毫秒
  │                                │
序列号 +1                       序列号归零
  │
序列号溢出(超过 4095)?
  │
  └─→ 等到下一毫秒再继续

最后把四段数据用位运算拼在一起:

复制代码
ID = 时间戳偏移量 << 22
   | 数据中心ID  << 17
   | 机器ID      << 12
   | 序列号

完整实现代码

java 复制代码
public class SnowflakeIdGenerator {

    // ── 起始时间戳(2020-01-01 00:00:00 UTC)
    // 设置得越晚,41 位时间戳能撑得越久
    private static final long START_TIMESTAMP = 1577836800000L;

    // ── 各段占多少位
    private static final long SEQUENCE_BITS      = 12L;
    private static final long MACHINE_ID_BITS    = 5L;
    private static final long DATACENTER_ID_BITS = 5L;

    // ── 各段最大值(利用位运算:-1L 异或左移 n 位 = 低 n 位全 1)
    private static final long MAX_SEQUENCE      = ~(-1L << SEQUENCE_BITS);      // 4095
    private static final long MAX_MACHINE_ID    = ~(-1L << MACHINE_ID_BITS);    // 31
    private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS); // 31

    // ── 各段在 64 位中的起始偏移(从低位往高位数)
    private static final long MACHINE_ID_SHIFT    = SEQUENCE_BITS;                              // 12
    private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS;            // 17
    private static final long TIMESTAMP_SHIFT     = DATACENTER_ID_SHIFT + DATACENTER_ID_BITS;   // 22

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

    public SnowflakeIdGenerator(long machineId, long datacenterId) {
        if (machineId < 0 || machineId > MAX_MACHINE_ID)
            throw new IllegalArgumentException("machineId 范围:0 ~ " + MAX_MACHINE_ID);
        if (datacenterId < 0 || datacenterId > MAX_DATACENTER_ID)
            throw new IllegalArgumentException("datacenterId 范围:0 ~ " + MAX_DATACENTER_ID);

        this.machineId    = machineId;
        this.datacenterId = datacenterId;
    }

    public synchronized long nextId() {
        long now = System.currentTimeMillis();

        // ── 1. 检测时钟回拨
        if (now < lastTimestamp) {
            throw new RuntimeException(
                "检测到时钟回拨,回拨了 " + (lastTimestamp - now) + "ms,拒绝生成 ID"
            );
        }

        if (now == lastTimestamp) {
            // ── 2. 同一毫秒:序列号自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            if (sequence == 0) {
                // 序列号用尽,等到下一毫秒
                now = waitNextMillis(lastTimestamp);
            }
        } else {
            // ── 3. 新的毫秒:序列号归零
            sequence = 0L;
        }

        lastTimestamp = now;

        // ── 4. 位运算拼接,生成最终 ID
        return ((now - START_TIMESTAMP) << TIMESTAMP_SHIFT)
             | (datacenterId            << DATACENTER_ID_SHIFT)
             | (machineId               << MACHINE_ID_SHIFT)
             | sequence;
    }

    // 自旋等待,直到时间戳推进到下一毫秒
    private long waitNextMillis(long lastTimestamp) {
        long now = System.currentTimeMillis();
        while (now <= lastTimestamp) {
            now = System.currentTimeMillis();
        }
        return now;
    }

    // 从 ID 反解生成时间(排查线上问题时很有用)
    public static long parseTimestamp(long id) {
        return (id >> TIMESTAMP_SHIFT) + START_TIMESTAMP;
    }
}

使用:

java 复制代码
SnowflakeIdGenerator generator = new SnowflakeIdGenerator(1, 1);

long id = generator.nextId();
// 输出示例:1346044067632218112

// 反解生成时间
long ts = SnowflakeIdGenerator.parseTimestamp(id);
System.out.println(new Date(ts));  // Mon Feb 17 10:30:00 CST 2026

三、雪花算法的优缺点

优点

性能极高: 纯本地运算,无网络 IO,单机 400 万+/秒。

趋势有序: 时间戳在最高位,ID 整体随时间递增。写数据库时新记录总是追加在索引末尾,B+ 树分裂少,写入性能好。

信息可反解: ID 里包含了生成时间和机器信息,排查线上问题时可以直接从 ID 定位到是哪台机器、哪个时间段产生的。

无外部依赖: 不需要数据库、Redis、ZooKeeper,任何环境都能跑。


缺点

缺点 1:时钟回拨

这是雪花算法最大的硬伤。

服务器时间受 NTP 同步影响,可能会被往回调整几毫秒。如果回拨发生时刚好在生成 ID,同一毫秒可能被重复使用,导致 ID 重复或者直接抛异常拒绝服务。

三种常见的处理方式,各有取舍:

java 复制代码
if (now < lastTimestamp) {
    long diff = lastTimestamp - now;

    // 方案 1:直接抛异常(实现最简单,但业务会感知报错)
    throw new RuntimeException("时钟回拨 " + diff + "ms");

    // 方案 2:小幅回拨就等一等(容忍 5ms 以内,超过再报错)
    if (diff <= 5) {
        Thread.sleep(diff * 2);  // 等时钟追上来
        now = System.currentTimeMillis();
    } else {
        throw new RuntimeException("时钟回拨超过 5ms,拒绝生成");
    }
}
// 方案 3:美团 Leaf 的做法
// 启动时从 ZooKeeper 读取上次记录的时间戳,若发现回拨则拒绝启动并触发告警
// 把问题暴露在部署阶段,而不是运行时悄悄出错
缺点 2:机器 ID 需要提前分配

每台机器必须有唯一的 machineId(0 ~ 31)。手动配置容易出错,容器化之后更麻烦------Pod 随时重建、IP 不固定,无法用 IP 末位当 machineId。

常见做法是用 ZooKeeper 创建临时顺序节点,节点序号自动成为 machineId:

java 复制代码
// 每次服务启动时创建临时顺序节点,节点序号即为机器 ID
// 服务下线后节点自动删除,ID 可被复用
String node = zk.create(
    "/snowflake/worker-",
    new byte[0],
    ZooDefs.Ids.OPEN_ACL_UNSAFE,
    CreateMode.EPHEMERAL_SEQUENTIAL   // 临时顺序节点
);

// "/snowflake/worker-0000000003" → machineId = 3
int machineId = Integer.parseInt(node.replace("/snowflake/worker-", "")) % 32;
缺点 3:同一毫秒内不严格有序

同一毫秒内最多 4096 个 ID,这 4096 个 ID 的顺序只反映生成顺序,不反映业务逻辑顺序。如果业务需要"先下的订单 ID 一定更小",雪花算法无法保证。


四、开源实现推荐

自己手写雪花算法容易踩细节坑,建议直接用成熟的库。

Hutool(轻量首选)

复制代码
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-core</artifactId>
    <version>5.8.26</version>
</dependency>
java 复制代码
// 已内置时钟回拨处理,开箱即用
Snowflake snowflake = IdUtil.getSnowflake(1, 1);
long id = snowflake.nextId();

10 行以内搞定,一般项目足够用了。

美团 Leaf(大规模分布式首选)

同时支持号段模式和雪花模式,内置 ZooKeeper 自动分配机器 ID,有完善的监控和告警。适合有一定体量、对稳定性要求高的项目。

百度 UidGenerator(超高并发场景)

用 RingBuffer 预生成 ID,后台线程异步补充,吞吐量比标准雪花算法高出数倍。适合极端高并发场景,但引入的复杂度也更高。


五、怎么选?

场景 推荐方案
单体应用、并发一般 数据库自增,别过度设计
需要有序 ID,已有数据库 号段模式(Leaf 号段版)
微服务、高并发、无中心依赖 雪花算法(Hutool)
大规模分布式、有 ZK 基础设施 美团 Leaf 雪花版
极限高并发 百度 UidGenerator

实际项目里用得最多的还是雪花算法:部署简单、性能好、够用。时钟回拨问题直接用 Hutool,它已经处理好了,不用自己趟坑。

相关推荐
数据知道3 小时前
PostgreSQL:Citus 分布式拓展,水平分片,支持海量数据与高并发
分布式·postgresql·wpf
洛豳枭薰16 小时前
分布式事务进阶
分布式
无心水16 小时前
5、微服务快速启航:基于Pig与BladeX构建高可用分布式系统实战
服务器·分布式·后端·spring·微服务·云原生·架构
闲人编程19 小时前
Redis分布式锁实现
redis·分布式·wpf·进程··死锁·readlock
yangyanping2010821 小时前
系统监控Prometheus之监控原理和配置
分布式·架构·prometheus
之歆21 小时前
ZooKeeper 分布式协调服务完全指南
分布式·zookeeper·wpf
之歆1 天前
DRBD 分布式复制块设备指南
分布式
时艰.1 天前
分布式 ID 服务实战
java·分布式
黄俊懿2 天前
【架构师从入门到进阶】第一章:架构设计基础——第二节:架构设计原则
分布式·后端·中间件·架构