高并发、分布式场景下的 ID 生成策略
探讨了高并发分布式环境下多种 ID 生成策略,包括 UUID、雪花算法、数据库自增、Redis 计数、号段模式及 Leaf 系统。分析了各方案的优缺点、性能指标及适用场景,提供了选型建议与优化方案,旨在帮助开发者根据业务规模和技术需求选择合适的 ID 生成机制。

探讨了高并发分布式环境下多种 ID 生成策略,包括 UUID、雪花算法、数据库自增、Redis 计数、号段模式及 Leaf 系统。分析了各方案的优缺点、性能指标及适用场景,提供了选型建议与优化方案,旨在帮助开发者根据业务规模和技术需求选择合适的 ID 生成机制。

在很多项目刚开始的时候,生成 ID 似乎是一件非常简单的事情。数据库里的 AUTO_INCREMENT 往往就能满足需求,开发者也很少会专门去思考这个问题。但当系统逐渐发展,访问量越来越大,甚至开始拆分成多个服务、部署在多台机器上时,一个看似简单的问题就会变得复杂起来:在高并发、分布式的环境下,如何生成一个既唯一又高效的 ID?
如果处理不好,就可能出现 ID 冲突、数据库性能瓶颈,甚至影响整个系统的稳定性。因此,在实际的后端开发中,ID 生成往往会被单独设计成一套机制。从最简单的数据库自增,到使用 Redis 计数器,再到像 Snowflake 这样的分布式算法,不同的方案都有各自的适用场景。
UUID(Universally Unique Identifier)是一个 128 位的全局唯一标识符,通常以 32 个十六进制数字表示,分为 5 组,例如:550e8400-e29b-41d4-a716-446655440000。Java 中可以使用 UUID.randomUUID().toString() 生成(版本 4,随机生成)。
注意:虽然 UUID 可以解决分布式唯一性问题,但由于其无序性和长度问题,通常不建议作为数据库主键使用。在高并发写入场景下,性能影响显著。
雪花算法是 Twitter 开源的分布式 ID 生成算法,生成的 ID 是一个 64 位的长整数,具有趋势递增、全局唯一的特点,适合在高并发分布式环境中使用。
雪花算法生成的 64 位 ID 由以下几部分组成:
| 位数 | 含义 | 说明 |
|---|---|---|
| 1 | 符号位 | 保证为正数,固定为 0 |
| 41 | 时间戳 | 毫秒级时间戳,从自定义 epoch 开始计算 |
| 10 | 机器 ID | 可配置为机房 ID + 机器 ID,唯一标识节点 |
| 12 | 序列号 | 同一毫秒内的自增序列(0-4095) |
(时间戳 << 22) | (机器 ID << 12) | 序列号/**
* 雪花算法 ID 生成器
*/
public class SnowflakeIdGenerator {
private final long epoch = 1609459200000L; // 2021-01-01 00:00:00
private final long workerIdBits = 10L;
private final long sequenceBits = 12L;
private final long maxWorkerId = ~(-1L << workerIdBits); // 1023
private final long maxSequence = ~(-1L << sequenceBits); // 4095
private final long workerIdShift = sequenceBits;
private final long timestampShift = sequenceBits + workerIdBits;
private long lastTimestamp = -1L;
private long sequence = 0L;
public synchronized long nextId() {
long timestamp = currentTimeMillis();
// 检查时钟回拨
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨异常,拒绝生成 ID");
}
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & maxSequence;
if (sequence == 0) {
// 同一毫秒内序列号用尽,等待下一毫秒
timestamp = waitNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - epoch) << timestampShift) | (workerId << workerIdShift) | sequence;
}
private long waitNextMillis(long lastTimestamp) {
long timestamp = currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = currentTimeMillis();
}
return timestamp;
}
}
数据库自增 ID 是最简单的 ID 生成方式,通过在表中设置 AUTO_INCREMENT 字段,由数据库自动维护 ID 的递增。
在单机 MySQL 中,使用 AUTO_INCREMENT 非常简单:
CREATE TABLE `orders` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`data` VARCHAR(255),
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
在分布式数据库(分库分表)环境中,直接使用 AUTO_INCREMENT 会导致ID 冲突:
通过一个独立的存根表(Stub Table) 集中分配 ID,解决分布式 ID 冲突问题。
CREATE TABLE `sequence_id` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`stub` CHAR(1) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY `stub` (`stub`)
) ENGINE=InnoDB COMMENT='ID 分配存根表';
stub:业务标识(如 'order'、'user'),唯一索引保证每个业务独立序列id:当前最大 ID,自增字段-- 原子操作:如果 stub 存在则 id+1,不存在则插入
INSERT INTO sequence_id (stub) VALUES ('order') ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id + 1);
INSERT INTO orders (id, data) VALUES (LAST_INSERT_ID(), '订单数据');
LAST_INSERT_ID() 是 MySQL 的会话级函数,返回当前连接最后生成的自增 ID适用场景:中小型系统,ID 生成频率不高(< 1000 QPS),对数据库依赖较强的传统架构。
Redis 作为高性能内存数据库,提供原子递增命令,可以用于实现分布式环境下的全局 ID 生成。
# 基本递增,返回递增后的值
INCR id:counter
# 指定步长递增
INCRBY id:counter 100
# 设置初始值(如果 key 不存在)
SET id:counter 1000 NX
public class RedisIdGenerator {
private Jedis jedis;
public Long nextId(String bizType) {
String key = "id:counter:" + bizType;
return jedis.incr(key);
}
}
结合时间戳保证 ID 趋势递增:
public Long nextId(String bizType) {
String date = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); // yyyyMMdd
String key = "id:counter:" + bizType + ":" + date;
Long seq = jedis.incr(key); // 组合:日期 + 序列号,如 202603160001
return Long.parseLong(date + String.format("%04d", seq));
}
INCR 操作天然原子性注意:对于超高频 ID 生成场景(>10 万 QPS),建议采用号段模式或雪花算法,避免 Redis 成为瓶颈。
号段模式(Segment)是一种批量预分配的 ID 生成方案,核心思想是:从数据库批量获取一段 ID(如 1001~2000),缓存在应用本地,然后逐个分配。这样将数据库的每次 ID 申请压缩为批量申请,极大减少数据库压力。
CREATE TABLE id_generator (
biz_tag VARCHAR(50) PRIMARY KEY COMMENT '业务标识',
max_id BIGINT NOT NULL COMMENT '当前已分配的最大 ID',
step INT NOT NULL COMMENT '步长(每次分配的 ID 数量)',
version BIGINT NOT NULL DEFAULT 0 COMMENT '版本号(乐观锁)',
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB COMMENT='ID 生成器配置表';
字段说明:
biz_tag:业务类型(如'order'、'user'),唯一标识不同业务序列max_id:当前已分配的最大 ID,下次分配从 max_id+1 开始step:步长,决定每次预分配的 ID 数量version:乐观锁版本,防止并发更新冲突根据业务量预估设置固定步长:
-- 高频业务:订单,步长 10 万
INSERT INTO id_generator (biz_tag, max_id, step) VALUES ('order', 0, 100000);
-- 低频业务:用户,步长 1000
INSERT INTO id_generator (biz_tag, max_id, step) VALUES ('user', 0, 1000);
根据历史消耗速率动态调整步长:
UPDATE id_generator SET max_id = max_id + step, version = version + 1, update_time = NOW() WHERE biz_tag = 'order' AND version = #{oldVersion};
并发控制:
SELECT max_id, step FROM id_generator WHERE biz_tag = 'order';
假设查询结果:max_id = 100000, step = 100000
则分配的号段为:[100001, 200000]
public class SegmentIdGenerator {
private AtomicLong currentId; // 当前分配的 ID
private long maxId; // 当前号段最大值
public synchronized long nextId() {
if (currentId.get() <= maxId) {
return currentId.getAndIncrement();
} else {
// 号段用完,申请新号段
allocateNewSegment();
return nextId();
}
}
private void allocateNewSegment() {
// 从数据库申请新号段
Segment segment = db.allocateSegment("order");
currentId.set(segment.getStartId()); // 100001
maxId = segment.getEndId(); // 200000
}
}
问题:号段用尽时才申请新号段,导致请求阻塞。
解决方案:
最佳实践:步长设置需要权衡,太小导致频繁访问数据库,太大导致 ID 浪费和重启丢失更多 ID。建议根据业务 QPS 设置步长为 5-10 分钟的消耗量。
Leaf 是美团开源的一套分布式 ID 生成系统,提供了Leaf-Segment和Leaf-Snowflake两种模式,在实际生产环境中广泛应用。
Leaf 采用 RESTful API 提供服务,支持以下特性:
在基础号段模式上,Leaf-Segment 引入了以下优化:
public class DoubleBuffer {
private SegmentBuffer currentBuffer; // 当前使用的 Buffer
private SegmentBuffer nextBuffer; // 预备 Buffer
public synchronized long nextId() {
if (currentBuffer.hasId()) {
return currentBuffer.nextId();
}
if (nextBuffer != null && nextBuffer.isReady()) {
// 切换 Buffer
currentBuffer = nextBuffer;
nextBuffer = null;
// 异步加载下一个 Buffer
loadNextBufferAsync();
return currentBuffer.nextId();
}
// 两个 Buffer 都为空,等待加载
waitForBuffer();
return nextId();
}
private void loadNextBufferAsync() {
executor.submit(() -> {
Segment segment = db.allocateSegment(bizTag);
SegmentBuffer buffer = new SegmentBuffer(segment);
nextBuffer = buffer;
});
}
}
工作流程:
loadNextBufferAsync()根据历史消耗速率自动调整步长:
CREATE TABLE leaf_alloc (
biz_tag VARCHAR(128) NOT NULL PRIMARY KEY COMMENT '业务标识',
max_id BIGINT NOT NULL COMMENT '当前最大 ID',
step INT NOT NULL COMMENT '步长',
description VARCHAR(256) COMMENT '业务描述',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;
Leaf-Snowflake 在原生雪花算法基础上,解决了时钟回拨和机器 ID 分配两大痛点。
public class WorkerIdAssigner {
private ZooKeeper zk;
public int assignWorkerId() {
// 1. 在 ZooKeeper 创建临时顺序节点
String path = zk.create("/leaf/snowflake/worker-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// 2. 解析节点序号作为 workerId
int workerId = parseWorkerId(path);
// 3. 注册监听,节点删除时重新分配
zk.exists(path, event -> {
if (event.getType() == EventType.NodeDeleted) {
reassignWorkerId();
}
});
return workerId;
}
}
优势:
Leaf-Snowflake 采用多层次时钟回拨处理策略:
public class LeafSnowflakeIdGenerator {
private long lastTimestamp = -1L;
public synchronized long nextId() {
long timestamp = timeGen();
// 时钟回拨检测
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset <= 100) {
// 轻度回拨:等待
waitUntilReach(lastTimestamp);
timestamp = timeGen();
} else if (offset <= 1000) {
// 中度回拨:校验备用时钟
if (checkBackupClock()) {
waitUntilReach(lastTimestamp);
timestamp = timeGen();
} else {
throw new ClockMovedBackwardsException("时钟回拨过大");
}
} else {
// 重度回拨:暂停服务
serviceStatus = ServiceStatus.SUSPENDED;
throw new ClockMovedBackwardsException("严重时钟回拨,服务暂停");
}
}
// ... 正常生成 ID 逻辑
}
}
客户端 → 负载均衡器 → [Leaf 节点 1, Leaf 节点 2, Leaf 节点 3]
↓
[ZooKeeper 集群] ←→ [MySQL 集群]
| 特性 | Leaf-Segment | Leaf-Snowflake |
|---|---|---|
| QPS | 10 万+ | 5 万+ |
| 延迟 | <1ms | <2ms |
| 连续性 | 段内连续,段间跳跃 | 趋势连续 |
| 依赖 | MySQL | ZooKeeper + 时钟服务 |
| 适用场景 | 业务维度 ID,高频生成 | 全局唯一 ID,需要时间信息 |
Leaf 系统将学术界分布式 ID 生成理论转化为工业级解决方案,主要贡献在于:
GitHub 地址:https://github.com/Meituan-Dianping/Leaf
生产建议:对于大多数互联网公司,Leaf-Segment 方案已能满足 90% 以上的场景。如果对 ID 的时间信息有要求,或需要严格的全局递增,可以考虑 Leaf-Snowflake。
| 方案 | 唯一性 | 有序性 | 性能 | 可用性 | 复杂度 | 适用场景 |
|---|---|---|---|---|---|---|
| UUID | 全局唯一 | 无序 | 极高 | 极高 | 极低 | 临时令牌、会话 ID、不入库标识 |
| 数据库自增 | 单库唯一 | 严格递增 | 低(依赖 DB) | 中(单点) | 低 | 单机应用、小型系统 |
| 数据库存根表 | 全局唯一 | 严格递增 | 中(依赖 DB) | 中(单点) | 中 | 中小型分布式系统 |
| Redis 原子计数 | 全局唯一 | 严格递增 | 高(依赖 Redis) | 中(Redis 集群) | 低 | 中小型系统,QPS<5 万 |
| 雪花算法 | 全局唯一 | 趋势递增 | 极高(本地) | 高(依赖时钟) | 中 | 大型分布式系统,需要时间信息 |
| 号段模式 | 全局唯一 | 段内连续 | 极高(内存) | 高(依赖 DB) | 中 | 高频 ID 生成,电商、支付 |
| Leaf-Segment | 全局唯一 | 段内连续 | 极高(内存) | 极高(高可用) | 高 | 生产环境,需要完善监控管理 |
| Leaf-Snowflake | 全局唯一 | 趋势递增 | 高(本地) | 极高(高可用) | 高 | 生产环境,需要时间信息 |
[时间戳][分片 ID][序列号]从简单方案迁移到复杂方案时:
选择 ID 生成方案时,需要综合考虑:
黄金法则:没有最好的方案,只有最适合的方案。从小规模开始,随着业务增长逐步演进,避免过度设计。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online