如何生成分布式唯一ID? | 从原理到实战的完整指南
目录导读
为什么需要分布式唯一ID?
在单体应用时代,使用数据库自增主键(如MySQL的AUTO_INCREMENT)足以应付所有ID生成需求,但进入分布式、微服务架构后,系统被拆分为多个独立服务,每个服务都有自己的数据库,此时面临三个核心问题:

- 分库分表场景下,无法保证全局唯一自增
- 高并发写入时,数据库自增会成为性能瓶颈
- 多个服务需要生成跨服务可用的唯一标识(如订单号、用户ID)
案例: 电商系统双11大促,订单服务每秒需生成10万+订单号,若用自增ID,数据库单点写入压力巨大,且分库后不同库的ID会重复。
分布式ID核心需求与挑战
一个好的分布式唯一ID方案必须满足:
| 需求 | 说明 | 反例 |
|---|---|---|
| 全局唯一 | 整个分布式系统内无重复 | UUID重复概率极低但非零 |
| 趋势递增 | 对数据库B+树索引友好 | 完全无序的ID会导致页分裂 |
| 高可用 | 生成服务不能因单点故障停止 | 依赖单一Redis的incr命令 |
| 高吞吐 | 支持10万+QPS生成 | 数据库自增值产生瓶颈 |
| 业务含义 | 可反解析时间戳、机器信息 | UUID无任何业务含义 |
六大主流方案详解
UUID(通用唯一识别码)
原理: 基于时间戳、MAC地址、随机数生成128位标识。
// Java示例 String id = UUID.randomUUID().toString(); // 形如:f47ac10b-58cc-4372-a567-0e02b2c3d479
优点:
- 实现极度简单,纯本地生成
- 性能极高,无网络开销
致命缺点:
- 完全无序:无法保证趋势递增,作为数据库主键会导致频繁页分裂(InnoDB B+树需要大量重排)
- 长度长:36字符含横杠,占用存储空间
- 无业务含义:不可解析时间或机器
适用场景:
- 非数据库主键的临时标识(如日志ID)
- 强调绝对唯一但不要求有序的场景
数据库自增ID/号段模式
原理1(自增): 利用数据库auto_increment特性。
原理2(号段): 每次从数据库取一个号段(如1000个ID),缓存到应用内存,用完再取下一段。
// 号段模式核心伪代码
public class IdGenerator {
private long currentMaxId; // 当前可用最大ID
private long step; // 号段步长
private long nextId; // 当前可用ID
public synchronized long nextId() {
if (nextId > currentMaxId) {
// 向数据库申请新号段,并更新currentMaxId
// UPDATE id_alloc SET max_id = max_id + step WHERE biz_tag = 'order'
// 乐观锁防止并发冲突
this.currentMaxId = maxIdFromDB + step;
this.nextId = maxIdFromDB + 1;
}
return nextId++;
}
}
优点:
- 严格递增,对数据库索引友好
- 可带业务含义(如不同业务分配不同号段)
缺点:
- 号段用完时存在数据库瓶颈(需更新行记录)
- 依赖数据库可用性,宕机则生成停止
改进: 使用双号段预缓存(美团Leaf方案),提前缓存下一个号段避免断层。
Redis INCR/INCRBY
原理: 利用Redis的原子自增操作。
# 每次执行产生唯一递增ID INCR order_id # 带业务前缀 INCR order:id:202405
优点:
- 性能极高(10万+QPS)
- 天然原子性,无重复
缺点:
- 依赖Redis持久化:若启用RDB快照+AOF,重启可能数据丢失导致ID重复
- 受Redis单机性能限制(可集群但增加复杂度)
- 每生成一个ID需一次网络RTT
改进: 使用Lua脚本批量生成连续ID,减少网络开销。
雪花算法(Snowflake,最经典方案)
原理: 生成一个64位long类型ID,结构如下:
0 | 41位时间戳 | 10位机器ID | 12位序列号
符号位(1bit) 毫秒级时间 workerID 0~4095自旋
算法要点:
- 41位时间戳:使用当前时间与自定义纪元(如2010年11月4日)的差值,可支持约69年
- 10位机器ID:支持1024个节点(5位数据中心+5位机器)
- 12位序列号:同一毫秒内可生成4096个不同ID,若耗尽则等待下一毫秒
Java核心实现(简化版):
public class SnowflakeIdWorker {
private long workerId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨异常");
}
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & 4095;
if (sequence == 0) timestamp = tilNextMillis(lastTimestamp);
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - EPOCH) << 22) | (workerId << 12) | sequence;
}
}
优点:
- 趋势递增、高性能(纯本地运算,无网络)
- 可反解析时间戳和机器信息
- 支持高并发(单毫秒4096个)
痛点:
- 时钟回拨问题:若服务器NTP时钟回拨,可能生成重复ID
- 机器ID需集中分配或自动选举
典型改进:
- 百度UidGenerator:依赖数据库+预取序列号
- 美团Leaf:Snowflake+号段双模式
美团Leaf(组合方案)
双模式支持:
- Leaf-Snowflake:基于雪花算法,使用ZooKeeper/Etcd自动分配workId,并记录上次时间戳到本地文件,若发现时钟回拨等待或抛出异常
- Leaf-Segment:号段模式+双buffer预取(提前缓存下个号段到内存)
设计亮点:
- 时钟回拨时等待时间同步完成
- 数据库号段使用乐观锁解决并发冲突
百度UidGenerator(增强版雪花)
原理:
- 基于Snowflake但使用RingBuffer预生成(类似双buffer)
- workId通过数据库的自增ID自动分配
- 支持秒级时间戳(节省位数),序列号使用滚动自增
特点:
- 解决时钟回拨:使用时间戳+滚动缓冲区
- 单机QPS可达30万+
方案对比与选型建议
| 方案 | 唯一性 | 有序性 | 可用性 | 性能 | 适用场景 |
|---|---|---|---|---|---|
| UUID | 极低概率重复 | 完全无序 | 极高 | 极高 | 非主键临时标识 |
| 数据库自增 | 严格唯一 | 严格递增 | 低 | 低 | 小系统、单库 |
| 号段模式 | 严格唯一 | 趋势递增 | 中 | 高 | 业务需连续ID |
| Redis INCR | 严格唯一 | 严格递增 | 中 | 高 | 高并发但可接受Redis依赖 |
| 雪花算法 | 依赖时间+机器 | 趋势递增 | 中 | 极高 | 默认主流方案 |
| 美团Leaf | 严格唯一 | 趋势递增 | 高 | 极高 | 金融级、互联网大厂首选 |
| 百度UidGenerator | 严格唯一 | 趋势递增 | 高 | 极高 | 极致性能场景 |
选型三步法:
- 简单需求:若集群节点<100,可接受小概率时钟回拨 → 裸用雪花算法
- 中等需求:要求高可用、无回拨 → 使用Leaf-Snowflake(依赖ZK)
- 大厂生产:必须永不重复+毫秒级故障恢复 → 使用Leaf-Segment双buffer
常见问题QA
Q1:雪花算法时钟回拨了怎么办?
A:有三种处理策略:
- 等待:阻塞直到时钟回正(适合回拨小于1秒)
- 抛异常:人工介入修复(严格场景不可接受)
- 备用序列号:使用高位序列号段跳过回拨时间段(美团Leaf法)
Q2:如何自动分配机器ID(workId)?
A:常用两种方式:
- ZooKeeper/Etcd临时节点,启动时注册,获取唯一id
- 数据库自增:每次启动插入一行获取自增ID,但重启需注意复用策略
Q3:Redis做ID生成如何防止数据丢失?
A:必须开启AOF持久化(everysec模式)并配合RDB快照,但极端断电仍可能重复,建议雪花算法优先,Redis方案用于非关键业务。
Q4:不同业务如何生成带前缀的ID?
A:数据库号段方案天然支持biz_tag列区分;雪花算法可通过调整bit位分配(如用8位做业务类型),但会减少可用的机器或序列号位数。
Q5:生成ID的最佳长度是多少?
A:
- 数据库主键建议64位long(如雪花算法)
- 可读性需求用字符串(如订单号:202405081234000001)
- 绝不用UUID做主键,除非是NoSQL无关索引的场景
生成分布式唯一ID没有银弹:
- 追求极致有序选号段模式
- 追求高性能和简单选雪花算法并做好回拨防护
- 追求高可用且无时钟依赖选数据库号段+双buffer
实际生产中,推荐优先考虑 美团Leaf 或基于其原理的自研实现,它集合了雪花和号段的优点,对于大多数小型微服务团队,Snowflake + 手动配置workId已经足够解决99%的问题。