分布式ID选型
# 分布式 ID 怎么选:自增、UUID 还是雪花
单库单表的时候,主键自增挺好用。一旦分库分表,或者多实例并发写,自增就不够用了,不同库的 id 会重复。这时候就得换生成方式。这篇把常见的几种方案和它们的坑说清楚。
## 先说需求
选 ID 方案前,先明确几条:
- 全局唯一,这个不用说
- 趋势递增,对 B+ 树索引友好
- 不要太长,占空间也影响索引
- 生成要快,别成为瓶颈
- 不暴露业务量(比如自增 id 一看就知道你有多少订单)
这几条很难同时满足,得取舍。
## 方案一:数据库自增(单库)
简单,id 短,天然递增,索引友好。
问题是只能单库。分库了就得改步长:
```sql
-- 实例 1 从 1 开始,步长 2
auto_increment_increment = 2
auto_increment_offset = 1
-- 实例 2 从 2 开始,步长 2
auto_increment_increment = 2
auto_increment_offset = 2
```
能解决唯一性,但扩容麻烦,新增实例要调整步长和起始值。
## 方案二:UUID
Java 里一行 `UUID.randomUUID()` 就出来了,全局唯一,生成快,不用依赖中心服务。
问题是:
- **太长了**,36 个字符,字符串索引占空间
- **完全无序**,做主键会导致 B+ 树频繁分裂,插入性能差
- **可读性差**,排查问题时不好记
可以用 UUID v7(时间有序版本),或者把 UUID 转成 64 位再存,能缓解无序问题。但大多数场景,UUID 还是不太适合做主键。
## 方案三:雪花算法(Snowflake)
最常见的选择。64 位整数,结构是:
```
1 位符号 | 41 位时间戳 | 10 位机器 | 12 位序列
```
- 时间戳决定趋势递增
- 机器位保证不同实例不冲突
- 序列位解决同一毫秒的并发
单机每毫秒能生成 4096 个,够用。
优点:有序、短(64 位)、生成快、不依赖数据库。
**最大的坑是时钟回拨**。服务器时间往回跳,可能生成重复 id。处理方式:
- 检测到回拨,等待时间追上来
- 或者直接报错,让上层重试
- 用小回拨容忍、大回拨拒绝
机器位的分配也要规划好,别两台机器用了同一个 workerId。
## 方案四:号段模式
数据库维护一个号段表,每次取一批 id 缓存在内存里用:
```sql
UPDATE id_segment SET max_id = max_id + 1000 WHERE biz = 'order';
SELECT max_id FROM id_segment WHERE biz = 'order';
```
一次拿 1000 个,用完再拿。减少数据库压力,id 也是递增的。
问题是依赖数据库,数据库挂了就发不出新号段(有缓存能撑一阵)。号段用完之前要提前续,别等用完才取。
## 怎么选
- 单库小系统:数据库自增,够用
- 分库分表、要求有序:雪花算法
- 不想依赖中心服务、能接受无序:UUID(v7 更好)
- 需要严格递增、能接受依赖数据库:号段模式
大多数互联网业务,雪花算法是比较均衡的选择。
## 几个实践建议
- 机器位用配置中心统一分配,别手动填
- 时钟回拨要有处理逻辑,别无视
- id 生成服务要有监控,生成失败要告警
- 别把自增 id 直接暴露给前端,防止被推算业务量
- 如果对外需要短 id,可以加一层映射(如短链)
## 小结
ID 方案没有最好的,只有合适的。核心权衡是:有序性、长度、依赖、复杂度。雪花算法在有序和性能上平衡得好,但时钟回拨必须处理。
选之前先想清楚你的表和并发规模,别为了「先进」而上复杂方案。单库能自增的,没必要一上来就雪花。
- 点赞
- 收藏
- 关注作者
评论(0)