跳到主要内容

分布式 ID 生成

问题

分库分表后如何保证主键全局唯一?雪花算法和号段模式分别是什么原理?各方案如何选择?

答案

为什么需要分布式 ID

分库分表后,各个库的自增 ID 会重复。需要一种全局唯一的 ID 生成方案。

分布式 ID 的要求

要求说明
全局唯一任何节点生成的 ID 不能重复
趋势递增有利于 B+ 树索引写入性能
高性能单机万级/秒以上
高可用不能单点故障
信息安全不暴露业务量(不能连续)

方案对比

方案唯一性有序性性能可用性长度
UUID❌ 无序高(本地)36 字符
自增 + 步长8 字节
雪花算法✅ 趋势递增高(本地)8 字节
号段模式8 字节
Redis INCR中(依赖 Redis)8 字节

UUID

550e8400-e29b-41d4-a716-446655440000
优势劣势
本地生成,无网络调用36 字符,太长
全局唯一无序,B+ 树频繁页分裂
无依赖不可读

结论:不适合做数据库主键(太长、无序),适合做分布式环境中的唯一标识符。

数据库自增 + 步长

-- 两个库设置不同起始值和步长
-- 库 1:1, 3, 5, 7, 9 ...
SET auto_increment_offset = 1;
SET auto_increment_increment = 2;

-- 库 2:2, 4, 6, 8, 10 ...
SET auto_increment_offset = 2;
SET auto_increment_increment = 2;
优势劣势
实现简单扩容困难(新增库需调整步长)
ID 有序依赖数据库

雪花算法(Snowflake)

Twitter 开源的分布式 ID 生成算法,生成 64 位 Long 型 ID

0 | 0000000000 0000000000 0000000000 0000000000 0 | 00000 | 00000 | 000000000000
1 | 41 位时间戳 | 5位 | 5位 | 12 位序列号
符号| 毫秒级(约 69 年) |数据中心| 机器ID| 每毫秒 4096 个
部分位数说明
符号位1固定 0
时间戳41毫秒级,可用约 69 年
数据中心 ID5支持 32 个数据中心
机器 ID5每个数据中心 32 台机器
序列号12每毫秒 4096 个 ID

理论 QPS:单机每秒可生成 409.6 万 个 ID。

public class SnowflakeIdGenerator {
private final long datacenterId;
private final long machineId;
private long sequence = 0L;
private long lastTimestamp = -1L;

// 起始时间戳(2024-01-01)
private static final long EPOCH = 1704067200000L;

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

// 时钟回拨检测
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨,拒绝生成 ID");
}

if (timestamp == lastTimestamp) {
// 同一毫秒内,序列号递增
sequence = (sequence + 1) & 0xFFF; // 12 位
if (sequence == 0) {
// 序列号溢出,等待下一毫秒
timestamp = waitNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}

lastTimestamp = timestamp;

return ((timestamp - EPOCH) << 22)
| (datacenterId << 17)
| (machineId << 12)
| sequence;
}
}

雪花算法的问题

问题说明解决方案
时钟回拨NTP 校时导致时间回退等待、报错、Leaf 的缓冲机制
机器 ID 分配需要保证全局唯一ZooKeeper 分配、配置文件

号段模式

预先从数据库获取一段 ID 范围,在内存中分配:

-- 号段分配表
CREATE TABLE id_alloc (
biz_tag VARCHAR(50) PRIMARY KEY, -- 业务标识
max_id BIGINT NOT NULL, -- 当前最大 ID
step INT NOT NULL, -- 步长
update_time TIMESTAMP
);

-- 获取号段(原子操作)
UPDATE id_alloc SET max_id = max_id + step WHERE biz_tag = 'order';
-- 假设 step = 1000,更新后 max_id = 5000
-- 则本次获取的号段为 [4001, 5000]

双 Buffer 优化:当前号段用到 10% 时,异步加载下一个号段到备用 Buffer,实现无阻塞。

Leaf(美团开源)

Leaf 是美团开源的分布式 ID 生成服务,同时支持号段模式雪花算法模式

模式特点
Leaf-segment(号段)双 Buffer + DB、支持动态步长调整
Leaf-snowflake(雪花)ZooKeeper 管理 workerID、解决时钟回拨

各场景推荐

场景推荐方案原因
简单场景雪花算法本地生成,无依赖
大规模分布式Leaf 号段模式高性能、高可用
要求连续 ID号段模式ID 趋势递增
不暴露信息雪花算法ID 中编码了时间,但不连续

常见面试问题

Q1: 雪花算法时钟回拨怎么解决?

答案

  1. 短暂等待:回拨时间 < 5ms,sleep 等待
  2. 拒绝服务:回拨时间过长,抛异常
  3. Leaf 方案:Leaf-snowflake 会在 ZooKeeper 记录上次时间戳,启动时比对,发现回拨则等待
  4. 百度 UidGenerator:预先生成 ID 放入 RingBuffer,消耗时从 Buffer 取,不实时依赖时钟

Q2: 号段模式的步长设多少合适?

答案

根据 QPS 动态调整:

  • 低 QPS<100/s):step = 1000
  • 高 QPS>1000/s):step = 10000 或更大

Leaf 支持动态步长:根据上一个号段的消耗速度自动调整下一个号段的大小。

Q3: 为什么雪花算法不适合用订单号对外暴露?

答案

雪花算法的 ID 包含时间戳和机器信息,虽然不是连续数字,但仍然可以:

  1. 从 ID 推算出大致的生成时间
  2. 通过 ID 的增长速度推算出业务量

如果需要对外暴露,可以:

  • 对雪花 ID 做加密/混淆
  • 使用独立的订单号(如 日期+随机数+校验位

Q4: Redis INCR 生成 ID 有什么问题?

答案

  1. 强依赖 Redis:Redis 宕机则无法生成 ID
  2. 持久化风险:Redis 使用 RDB 持久化,宕机可能丢失最新的 ID 值导致重复
  3. 性能瓶颈:每次生成都是网络调用

改进:Redis 每次获取一段(如 +1000),本地内存中分配,类似号段模式。

相关链接