🧠 ID 生成方案深度笔记(基于实际分布式架构场景)
一、ID 生成的本质
ID 生成的核心可归纳为两类:
| 类型 | 描述 |
|---|---|
| ✅ 自增型 | 为了保持索引有序(如B+树),多数选择趋势递增、自增型 ID(利于写入性能) |
| ✅ 随机型 | 随机不可预测,如 UUID,适合唯一性要求高但不要求顺序的场景 |
核心本质:是否趋势递增 + 是否分布式唯一
二、为什么多数系统偏好“趋势递增”ID?
因为多数数据库采用 B+树索引结构(如 InnoDB 主键索引),使用无序 UUID 容易导致频繁的页分裂与重构,从而带来:
-
写放大
-
索引维护成本高
-
页分裂频繁,造成性能瓶颈
所以生产中常常采用时间戳 + 某种局部唯一机制,实现趋势递增 ID。
三、雪花算法(Snowflake)详解
雪花 ID 的组成结构(64 位):
| 1bit 符号 | 41bit 时间戳 | 10bit 机器信息 | 12bit 序列号 |
| 字段 | 说明 |
|---|---|
| 符号位 | 永远为 0 |
| 时间戳 | 当前时间戳 - 固定起始时间(通常是项目上线时间) |
| 机器位 | 数据中心 + 机器编号(共10位) |
| 序列号 | 同一毫秒内自增,避免并发冲突(最多支持 4096 个) |
雪花算法的关键痛点 —— 时间回拨
时间回拨会导致生成的时间戳小于上一次生成的 ID,导致重复 ID、顺序错乱。
四、时间回拨的解决方案整理
| 方案 | 描述 |
|---|---|
| ❌ 拒绝生成 ID | 一旦发现当前时间戳小于 lastTimestamp,直接抛异常拒绝生成 ID |
| ⏱ 等待系统时间恢复 | 使用 Thread.sleep() 等待时间追上 lastTimestamp |
| 🚩 使用回拨标记位(标志位) | 多保留一位标识“回拨状态”,时间倒退时设置该标志,保证 ID 唯一但不影响正常序列 |
| 🕓 引入统一时间服务(如 Redis) | 所有服务通过 Redis 获取统一时间戳,避免本地时钟差异 |
| 🌊 使用容错窗口(Tolerate) | 允许小范围时间回拨(如 ≤5ms),不影响 ID 分配,超过则拒绝发号 |
时间回拨的发现方式:
每次生成 ID 时会保存一个 lastTimestamp,与当前时间 now 做比较:
if (now < lastTimestamp) {
// 时间回拨
}
五、Leaf Segment 模式详解(美团)
基本机制:
-
每个业务有一张记录表,如:
| biz_tag | max_id | step |
|---|---|---|
| order_id | 10000 | 1000 |
-
应用启动时,向数据库申请号段
[10001 ~ 11000] -
本地使用 AtomicLong 做发号,使用完再拉新段
-
可缓存两段,提前加载,避免请求阻塞
Leaf 的特点:
| 优点 | 缺点 |
|---|---|
| ✅ 趋势递增,利于写入索引 | ❌ ID 仍然是连续的,容易被枚举 |
| ✅ 多业务隔离,按 tag 管理 | ❌ 必须依赖数据库作为中心,不能完全无中心 |
| ✅ 本地内存生成,性能极高(内存 AtomicLong) | ❌ 使用前需要分段估计数值范围(如 1000 个够不够) |
六、段号机制设计思考
你提出的重要问题:
段号怎么知道请求要多少?如果一个请求涉及多个业务怎么办?
正确实践:
-
通常只会一次性申请一个段(如 1000 个 ID),本地发号,用完了再拉段。
-
每个业务线程会对段对象加锁,保证并发安全。
-
缓存中维护当前段 + 预加载段,异步补充。
class SegmentBuffer {
volatile long current;
volatile long max;
AtomicLong idCursor;
}
七、自增 ID 的安全风险与美团未解决的问题
核心问题:
虽然 Leaf 解决了分布式自增问题,但 ID 仍连续递增,易被枚举攻击,如:
/order/10001
/order/10002
/order/10003
八、安全防御措施
🔒 后端防御:ID 不等于权限
-
所有查询 必须加 userId 或 tenantId
-
不信任前端传来的 ID,先查属主再处理
-
每个 Controller 都要校验数据归属权(或统一拦截器 AOP 处理)
SELECT * FROM order WHERE id = ? AND user_id = ?
🔐 前端防御:ID 映射或脱敏
| 方式 | 说明 |
|---|---|
| Hashids | 使用盐将整数 ID 编码为不可预测短串,如 123 → XyP9A2 |
| base62+加盐 | 自定义算法进行不可逆映射 |
| token 映射 | 为 ID 提供一个随机 token,前端仅传 token,内部再映射 |
🧰 实现建议(Java):
-
封装一个
IDEncryptor工具类(如集成 Hashids) -
Controller 返回 ID 字段统一调用
encrypt() -
所有详情接口统一调用
decrypt()映射真实 ID
九、总结与最佳实践建议
| 目标 | 推荐方案 |
|---|---|
| 分布式趋势递增 | Snowflake 或 Leaf Segment |
| 不可预测性 | 对外 ID 加密(Hashids)或 token 映射 |
| 多节点部署 | 使用统一时间服务或容忍回拨策略 |
| 查询权限控制 | 所有查询都要带 userId,并校验数据归属 |
| 性能+安全 | Leaf Segment + ID 加密 是性能与安全的较优结合方案 |
🔚 最后一句总结
无论你选哪种发号策略,ID 本身不能承载权限语义。只有结合权限校验 + 加密映射,才能真正保障系统安全。
✅ ID 生成策略系统笔记
一、🌱 ID 生成的核心目标
-
全局唯一(防止主键冲突)
-
高性能生成(高并发下不会成为瓶颈)
-
趋势递增(有利于数据库写入和索引)
-
分布式可用(适用于多服务、多节点部署)
-
可读性/可控性(业务场景可能需要)
二、🌍 常见 ID 生成策略分类
| 分类 | 代表方案 | 是否递增 | 是否唯一 | 是否分布式支持 |
|---|---|---|---|---|
| 自增型 ID | MySQL AUTO_INCREMENT | ✅ 是 | ❌ 多库冲突 | ❌ 不适合 |
| 分布式递增 | Redis INCR, Leaf Segment | ✅ 是 | ✅ 是 | ✅ 是 |
| 时间戳型 ID | 雪花算法(Snowflake) | ✅ 近似 | ✅ 是 | ✅ 是 |
| 随机型 ID | UUID, NanoID | ❌ 否 | ✅ 是 | ✅ 是 |
| 业务拼接型 | 时间戳 + 机器号 + 随机串 | ✅ 否/可控 | ✅ 是 | ✅ 是 |
三、🔥 各 ID 方案详解
1. MySQL 自增主键(AUTO_INCREMENT)
-
优点:简单,天然递增
-
缺点:
-
多库环境 ID 冲突
-
聚簇索引写入集中在 B+树右边界,高并发下造成页锁争用
-
热点页写入 + 页分裂,性能下降
-
2. Redis INCR 自增 ID(推荐 ⭐)
📌 为什么要使用 Redis 自增?
-
✅ 解决数据库热点写入问题:MySQL 插入集中在右边界页 → 页锁竞争严重;Redis 完全内存操作,无页锁。
-
✅ 高并发、高性能:Redis 的
INCR是单线程、原子、极快的操作,可达 百万 QPS。 -
✅ 天然支持分布式 ID 统一发号:各个服务通过 Redis 同一个 key 发号,不存在重复。
-
✅ 灵活拼接业务 ID:可组合时间戳、业务类型前缀、自定义步长,实现趋势有序、格式可控的 ID。
示例:20250612-000001
生成规则:date(8位) + Redis:INCR 6位自增数
3. Snowflake 雪花算法
-
ID结构:符号位 + 时间戳 + 机器ID + 自增序列
-
优点:
-
无中心化依赖,每台机器可独立生成
-
趋势递增(方便数据库写入)
-
-
缺点:
-
时钟回拨要特别处理(否则可能生成重复 ID)
-
编码复杂,部署需标准化(需要发号器组件或网关代理)
-
4. Leaf Segment / Leaf Snowflake(美团)
-
Leaf Segment:从数据库中提前取一段号段缓存在本地
-
Leaf Snowflake:Snowflake 的中心化服务实现
-
优点:
-
把 ID 生成服务做成独立中间件(服务化)
-
高可用,容灾能力好
-
-
缺点:
-
运维成本高
-
系统依赖中心服务,复杂度高
-
5. UUID / NanoID 等随机型
-
优点:
-
保证全局唯一性(无中心、跨语言)
-
无需协调,自主生成
-
-
缺点:
-
无序,不适合做数据库主键(写入分裂严重)
-
占空间大(UUID 为 128-bit,36 字符)
-
四、🧩 各方案对比表格
| 方案类型 | 唯一性 | 有序性 | 分布式支持 | 写入性能 | 可读性 | 成熟度 |
|---|---|---|---|---|---|---|
| MySQL自增 | ❌ | ✅ | ❌ | ❌ 热点问题 | ✅ 简单 | ✅ 高 |
| Redis INCR | ✅ | ✅ | ✅ | ✅ 超快 | ✅ 灵活 | ✅ 高 |
| 雪花算法 | ✅ | ✅* | ✅ | ✅ | 中 | ✅ 高 |
| Leaf Segment | ✅ | ✅ | ✅ | ✅ 批处理 | ✅ | ✅ 成熟 |
| UUID / NanoID | ✅ | ❌ | ✅ | ❌ 无序插入 | ❌ | ✅ 高 |
五、🎯 Redis INCR 实战建议
💡 构建全局 ID 方案推荐结构:
ID = 前缀 + 日期(可选)+ Redis自增数
-
业务前缀:如
ORDER- -
日期:
yyyyMMdd格式 -
Redis key 示例:
order:id:20250612
示例 Java 伪代码:
String prefix = "ORDER";
String date = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
String redisKey = "id:" + prefix.toLowerCase() + ":" + date;
Long incr = redisTemplate.opsForValue().increment(redisKey);
String finalId = prefix + "-" + date + "-" + String.format("%06d", incr);
六、📌 总结一句话
在高并发、分布式、要求趋势递增的系统中,使用 Redis
INCR来生成自增 ID 是一种性能极高、架构灵活、实现简单的最佳实践方案,完美替代数据库主键自增的瓶颈问题。


被折叠的 条评论
为什么被折叠?



