前言
分库分表、多节点写入时,单机自增ID不够用了。订单号、用户ID、日志追踪ID都要全局唯一、尽量有序、高性能,还要考虑数据库压力和安全。市面上方案很多:UUID、雪花算法、号段模式、Redis 自增……到底选哪个?
本文从场景出发,对比常见分布式ID方案的特点和适用场景,并给出 Java/Go 下的实战示例,方便你在项目里直接选型、落地。
1. 先搞清楚:分布式ID要满足什么?
| 要求 | 说明 |
|---|---|
| 全局唯一 | 多节点、多表不能重复 |
| 趋势递增 | 利于 MySQL 主键索引、分库分表路由 |
| 高性能 | 本地生成、少依赖外部服务 |
| 高可用 | 不依赖单点,故障影响小 |
| 信息安全 | 尽量不暴露业务量、不连续可猜 |
不同业务侧重点不同:订单号要可读、防篡改;用户ID要短、可暴露;链路追踪要全局唯一、易生成。
2. 方案一:UUID
2.1 特点
- 优点:本地生成、无网络、实现简单、绝对不重复。
- 缺点:无序(随机 UUID)、36 位字符串、做 MySQL 主键会导致页分裂、索引效率差;无业务含义。
2.2 适用场景
- 临时凭证、一次性 token、日志追踪 ID。
- 不做主键、不按范围查询的场景。
2.3 示例
// Java
import java.util.UUID;
String id = UUID.randomUUID().toString(); // 550e8400-e29b-41d4-a716-446655440000
// Go
import "github.com/google/uuid"
id := uuid.New().String()
# 命令行
uuidgen
2.4 小结
适合「只要唯一、不关心顺序」的场景,不适合做 MySQL 主键或订单号。
3. 方案二:数据库自增(单库/多库步长)
3.1 单库自增
单库时用 AUTO_INCREMENT 即可,简单可靠。分库分表后各自自增会重复,不能直接当全局ID。
3.2 多库步长自增
每台库设置不同起点和步长,例如 2 台库:
- 库1:
auto_increment_increment=2,auto_increment_offset=1→ 1, 3, 5, 7… - 库2:
auto_increment_increment=2,auto_increment_offset=2→ 2, 4, 6, 8…
优点:实现简单、趋势递增。
缺点:扩容要调步长、迁移麻烦;强依赖数据库,写压力大。
适合节点数固定、写入量不大的场景,现在用得越来越少。
4. 方案三:号段模式(Segment)
4.1 原理
由中心服务(或数据库表)一次分配一段 ID(例如 1~1000),应用在内存里自增使用,用完了再取下一段。
[DB/服务] 分配 1~1000 → 应用A 本地 1,2,3...1000
分配 1001~2000 → 应用B 本地 1001,1002...
4.2 优点
- 数据库压力小(一次取一批)。
- ID 连续、趋势递增,适合做主键、分库分表。
- 可做双 buffer 预取,几乎无阻塞。
4.3 缺点
- 依赖中心服务或 DB;中心挂了要等恢复或提前多取几段。
- 会有「段内连续、段间可能浪费」的步长,一般可接受。
4.4 数据库表设计示例
CREATE TABLE segment_id (
biz_tag VARCHAR(32) PRIMARY KEY COMMENT '业务类型:order/user',
max_id BIGINT NOT NULL DEFAULT 0 COMMENT '当前最大ID',
step INT NOT NULL DEFAULT 1000 COMMENT '号段长度',
update_time DATETIME NOT NULL
);
-- 取号段(原子更新)
UPDATE segment_id SET max_id = max_id + step, update_time = NOW()
WHERE biz_tag = 'order';
-- 然后 SELECT max_id, step 得到 [max_id - step + 1, max_id]
4.5 Java 取号段示例(简化)
// 伪代码:取号段
public synchronized long nextId(String bizTag) {
if (current >= end) {
// 从 DB 拉取新号段
Segment seg = segmentDao.getNextSegment(bizTag);
current = seg.getMaxId() - seg.getStep();
end = seg.getMaxId();
}
return ++current;
}
4.6 小结
适合中等 QPS、希望少打 DB、且能接受依赖中心的业务,很多大厂内部 ID 服务就是这样做的。
5. 方案四:雪花算法(Snowflake)
5.1 结构(64 bit)
- 1 bit:符号位,固定 0。
- 41 bit:毫秒级时间戳,可用约 69 年。
- 10 bit:机器/节点 ID(最多 1024 个节点)。
- 12 bit:同一毫秒内序列(每毫秒最多 4096 个 ID)。
同一毫秒内超过 4096 可等待下一毫秒或扩展序列位。
5.2 优点
- 本地生成、无网络、高性能。
- 趋势递增,对 MySQL 主键和 B+ 树友好。
- 可包含时间信息,便于排查和粗略排序。
5.3 缺点
- 强依赖时钟:时钟回拨会导致重复或异常,需要做时钟回拨处理(等待/报错/换节点)。
- 机器 ID 要分配好,否则多节点可能重复。
5.4 Java 示例(简化版)
public class SnowflakeIdGenerator {
private final long workerId;
private final long epoch = 1609459200000L; // 2021-01-01 00:00:00
private long lastTimestamp = -1L;
private long sequence = 0L;
private static final long SEQUENCE_BITS = 12L;
private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS); // 4095
public synchronized long nextId() {
long now = System.currentTimeMillis();
if (now < lastTimestamp) {
throw new IllegalStateException("时钟回拨,拒绝生成ID");
}
if (now == lastTimestamp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0) {
now = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = now;
return ((now - epoch) << 22) | (workerId << 12) | sequence;
}
private long tilNextMillis(long last) {
long now = System.currentTimeMillis();
while (now <= last) now = System.currentTimeMillis();
return now;
}
}
5.5 Go 示例(第三方库)
import "github.com/bwmarrin/snowflake"
node, _ := snowflake.NewNode(1) // 节点ID 1
id := node.Generate() // 返回 int64
5.6 小结
适合高 QPS、多节点、能保证时钟可靠的场景,是当前最常用的分布式ID方案之一。生产务必处理时钟回拨(NTP、虚拟机挂起等)。
6. 方案五:Redis INCR
6.1 用法
INCR id:order
# 返回 1, 2, 3...
可加前缀做业务隔离:id:order、id:user。
6.2 优点
- 实现简单、性能高。
- 可设置过期或按日期 key,方便按天清零等策略。
6.3 缺点
- 依赖 Redis 可用性;持久化不当可能丢一段(AOF 可缓解)。
- 纯自增,无时间信息;要做成趋势递增需要 key 设计(如带日期)。
适合对绝对连续要求不高、已有 Redis 的辅助ID(如活动计数、短链ID)。
7. 方案对比与选型建议
| 方案 | 唯一性 | 有序性 | 性能 | 依赖 | 适用场景 |
|---|---|---|---|---|---|
| UUID | 是 | 否 | 高 | 无 | 临时ID、追踪ID、不做主键 |
| 库步长 | 是 | 是 | 中 | DB | 节点数固定、小规模 |
| 号段 | 是 | 是 | 高 | 中心/DB | 中高QPS、可接受中心依赖 |
| 雪花 | 是 | 趋势递增 | 高 | 时钟+节点ID | 高QPS、多节点、主键/订单 |
| Redis | 是 | 是 | 高 | Redis | 辅助ID、计数、短链 |
简单选型:
- 订单号、用户ID、主键:优先 雪花 或 号段;不能接受时钟风险选号段。
- 临时 token、链路 ID:UUID 即可。
- 已有 Redis、非主键:可用 Redis INCR 做补充。
8. 实战注意点
8.1 订单号可读性
雪花是数字,可直接用;若要带日期、业务前缀,可在前面拼字符串,例如:ORD202502141234567890(日期 + 雪花后几位或完整雪花转码)。
8.2 号段与雪花混用
- 主键、分库分表路由:用雪花或号段。
- 对外展示订单号:可在雪花前加前缀、或单独用号段/Redis 生成短号。
8.3 安全与防爬
- 不要用连续自增对外暴露;雪花或号段对外可做一次编码(如 Base62、洗牌算法)。
- 敏感接口加签名、限流,不单靠 ID 防刷。
9. 总结
- UUID:无顺序要求、不做主键时用。
- 号段:少打 DB、趋势递增、可接受中心服务时用。
- 雪花:高 QPS、多节点、主键/订单号首选,注意时钟回拨与节点ID分配。
- Redis:辅助ID、计数、短链等补充方案。
按业务选一种为主、其他补充,就能覆盖绝大多数后端分布式ID需求。