分布式 ID 方案
分布式 ID 需求
在分布式系统中,数据库自增 ID 无法满足需求,分布式 ID 需要满足:
text
1. 全局唯一 — 绝对不能重复
2. 趋势递增 — 利于数据库 B+ 树索引
3. 高可用 — 不能成为系统瓶颈
4. 高性能 — 生成速度要快(本地生成优于远程获取)
5. 可读性 — 最好包含业务或时间信息方案对比
| 方案 | ID 长度 | 趋势递增 | 性能 | 依赖 | 适用场景 |
|---|---|---|---|---|---|
| UUID | 36 位字符串 | ❌ 无序 | ★★★★★ | 无 | 临时 ID、文件名 |
| 雪花算法 | 19 位数字 | ✅ | ★★★★★ | 无(时钟同步) | 最常用 |
| 数据库号段 | 19 位数字 | ✅ | ★★★★ | 数据库 | 订单号、用户 ID |
| Leaf | 自定义 | ✅ | ★★★★ | ZK / 数据库 | 美团方案 |
| Redis INCR | 自增数字 | ✅ | ★★★★ | Redis | 低并发场景 |
UUID
格式
text
标准格式: 550e8400-e29b-41d4-a716-446655440000
- 时间戳(60 bit)
- 时钟序列(14 bit)
- 节点标识(48 bit)各版本
| 版本 | 生成方式 | 优劣 |
|---|---|---|
| UUID v1 | 时间戳 + MAC 地址 | 可追溯时间,暴露 MAC 地址 |
| UUID v4 | 随机数 | 最常用,无规律 |
| UUID v7 | 时间戳 + 随机数 | 新标准,趋势递增 |
Java 实现
java
// Java 内置
String uuid = UUID.randomUUID().toString();
// 结果: "550e8400-e29b-41d4-a716-446655440000"
// 去掉连字符(更紧凑)
String compact = uuid.replaceAll("-", "");
// 结果: "550e8400e29b41d4a716446655440000"问题
text
1. 无序 — 数据库 B+ 树插入频繁页分裂,性能下降
2. 存储空间大 — 36 位字符串 vs long 19 位
3. UUID v1 暴露 MAC 地址(安全风险)
4. 不是趋势递增,不适用于做分页/排序雪花算法(Snowflake)
Twitter 开源,是目前最广泛使用的分布式 ID 算法。
ID 结构
text
Snowflake ID = 64 位 long(19 位十进制)
0 | 0000000000 0000000000 0000000000 0000000000 0 | 00000 | 00000 | 000000000000
1 | 41 位时间戳 | 5 位 | 5 位 | 12 位序列号
↑ | |工作节点|数据中心| 同一毫秒内序号
符号位 (1024 台) (4096/毫秒)
始终为 0核心参数
| 字段 | 位数 | 范围 | 说明 |
|---|---|---|---|
| timestamp | 41 | 2^41 ≈ 69 年 | 相对于某个 epoch 的毫秒数 |
| datacenterId | 5 | 0-31 | 数据中心标识 |
| workerId | 5 | 0-31 | 工作节点标识 |
| sequence | 12 | 0-4095 | 同一毫秒内的自增序号 |
Java 实现
java
public class SnowflakeIdWorker {
// 起始时间戳(2026-07-01)
private static final long EPOCH = 1780300800000L;
private static final long DATACENTER_BITS = 5L;
private static final long WORKER_BITS = 5L;
private static final long SEQUENCE_BITS = 12L;
private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_BITS);
private static final long MAX_WORKER_ID = ~(-1L << WORKER_BITS);
private static final long WORKER_SHIFT = SEQUENCE_BITS;
private static final long DATACENTER_SHIFT = SEQUENCE_BITS + WORKER_BITS;
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_BITS + DATACENTER_BITS;
private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
private final long datacenterId;
private final long workerId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdWorker(long datacenterId, long workerId) {
if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
throw new IllegalArgumentException("datacenterId out of range");
}
if (workerId > MAX_WORKER_ID || workerId < 0) {
throw new IllegalArgumentException("workerId out of range");
}
this.datacenterId = datacenterId;
this.workerId = workerId;
}
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
if (timestamp < lastTimestamp) {
// 时钟回拨,等待或抛出异常
long offset = lastTimestamp - timestamp;
if (offset <= 5) {
Thread.sleep(offset + 1);
timestamp = System.currentTimeMillis();
} else {
throw new RuntimeException("Clock moved backwards");
}
}
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
if (sequence == 0) {
// 同一毫秒内序列用完,等待下一毫秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - EPOCH) << TIMESTAMP_SHIFT)
| (datacenterId << DATACENTER_SHIFT)
| (workerId << WORKER_SHIFT)
| sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
// 使用
SnowflakeIdWorker idWorker = new SnowflakeIdWorker(1, 1);
long id = idWorker.nextId(); // 例: 9417804888083456雪花算法的问题
| 问题 | 说明 | 解决方案 |
|---|---|---|
| 时钟回拨 | 服务器 NTP 时间同步导致 | 回拨小时等待,大时抛异常 |
| 节点号固定 | workerId 需要手动分配 | ZK / Redis 自动注册 |
| 序列号溢出 | 同一毫秒超过 4096 | 等待下一毫秒 |
美团 Leaf
Leaf 是美团开源的分布式 ID 生成方案,支持两种模式。
Leaf-segment(号段模式)
原理:数据库维护一个号段,Leaf Server 批量获取后本地分配。
sql
CREATE TABLE `t_leaf_alloc` (
`biz_tag` varchar(32) NOT NULL COMMENT '业务标识',
`max_id` bigint(20) NOT NULL DEFAULT '1',
`step` int(11) NOT NULL DEFAULT '1000' COMMENT '号段步长',
`description` varchar(256) DEFAULT NULL,
`update_time` datetime NOT NULL,
PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB;流程:
text
1. Leaf Server 启动,请求分配的号段:
UPDATE t_leaf_alloc SET max_id = max_id + step WHERE biz_tag = 'order'
返回 max_id = 1, 新号段 = [1, 1000]
2. Leaf Server 在内存中分配 ID:
1, 2, 3, ... , 1000
3. 当号段使用量 > 90% 时,异步预取下一个号段
避免号段耗尽时的同步等待
4. 重启后从数据库恢复号段优缺:
- 优点:ID 趋势递增、DB 压力小(批量获取)
- 缺点:DB 不可用时不可用
Leaf-snowflake
在标准雪花算法基础上,通过 Zookeeper 自动分配 workerId:
text
1. 启动时在 ZK 注册 EPHEMERAL 节点 /snowflake/{ip:port}
2. ZK 自动分配顺序号作为 workerId
3. 定期上传自身时间戳到 ZK
4. 从 ZK 获取其他节点时间戳,判断时钟回拨
5. 启动完成后,与标准雪花算法一致号段模式(自实现)
若不引入 Leaf,可直接在数据库中实现号段模式。
sql
CREATE TABLE `t_id_alloc` (
`biz_tag` varchar(32) NOT NULL COMMENT '业务标识',
`max_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '当前最大 ID',
`step` int(11) NOT NULL DEFAULT '1000' COMMENT '步长',
`version` int(11) NOT NULL DEFAULT '0' COMMENT '乐观锁',
PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB;java
@Service
public class IdSegmentService {
@Autowired
private JdbcTemplate jdbcTemplate;
// 缓存:<bizTag, 当前号段>
private final Map<String, IdSegment> cache = new ConcurrentHashMap<>();
public long nextId(String bizTag) {
IdSegment segment = cache.computeIfAbsent(bizTag, this::initSegment);
long id = segment.nextId();
if (id == -1) {
// 号段耗尽,同步加载新号段
synchronized (bizTag.intern()) {
segment = loadNewSegment(bizTag);
cache.put(bizTag, segment);
id = segment.nextId();
}
}
return id;
}
private synchronized IdSegment loadNewSegment(String bizTag) {
// 乐观锁更新 max_id
int rows = jdbcTemplate.update(
"UPDATE t_id_alloc SET max_id = max_id + step, version = version + 1 " +
"WHERE biz_tag = ? AND version = ?",
bizTag, getVersion(bizTag));
if (rows == 0) throw new RuntimeException("并发冲突");
long newMax = jdbcTemplate.queryForObject(
"SELECT max_id FROM t_id_alloc WHERE biz_tag = ?", Long.class, bizTag);
long step = jdbcTemplate.queryForObject(
"SELECT step FROM t_id_alloc WHERE biz_tag = ?", Long.class, bizTag);
return new IdSegment(newMax - step + 1, newMax);
}
}Redis INCR
java
@Component
public class RedisIdGenerator {
@Autowired
private StringRedisTemplate redisTemplate;
public long nextId(String key) {
// INCR:原子自增
Long id = redisTemplate.opsForValue().increment(key);
if (id == null) {
throw new RuntimeException("ID 生成失败");
}
return id;
}
}局限:
- 每次生成都要访问 Redis(网络开销)
- 无法保证趋势递增的连续性
- ID 可预测(安全风险)
选型建议
text
通用场景 → 雪花算法
- 高性能、无中心依赖
- 注意时钟回拨和 workerId 分配
需要趋势递增且可控 → Leaf-segment(号段模式)
- ID 连续、可读性好
- 适合订单号、用户 ID
- 需要维护 DB + ZK
临时唯一标识 → UUID
- 不需要有序性
- 文件名、traceId、sessionId
标准 UUID 趋势递增 → UUID v7(Java 21+)
- 兼顾 UUID 的唯一性和时间有序性生产建议
text
1. 雪花算法优先,IaaS 层 NTP 同步配置好
2. 订单号可用号段模式 + 日期前缀(如 202607270000001)
3. 不要用 UUID 做数据库主键(性能灾难)
4. 分布式 ID 生成服务需要独立部署,避免单点
5. 监控 ID 生成速率和号段消耗速度