目录
[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,它已经处理好了,不用自己趟坑。