ID 生成方案深度笔记


🧠 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 模式详解(美团)

基本机制:

  1. 每个业务有一张记录表,如:

biz_tagmax_idstep
order_id100001000
  1. 应用启动时,向数据库申请号段 [10001 ~ 11000]

  2. 本地使用 AtomicLong 做发号,使用完再拉新段

  3. 可缓存两段,提前加载,避免请求阻塞

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 生成的核心目标

  1. 全局唯一(防止主键冲突)

  2. 高性能生成(高并发下不会成为瓶颈)

  3. 趋势递增(有利于数据库写入和索引)

  4. 分布式可用(适用于多服务、多节点部署)

  5. 可读性/可控性(业务场景可能需要)


二、🌍 常见 ID 生成策略分类

分类代表方案是否递增是否唯一是否分布式支持
自增型 IDMySQL AUTO_INCREMENT✅ 是❌ 多库冲突❌ 不适合
分布式递增Redis INCR, Leaf Segment✅ 是✅ 是✅ 是
时间戳型 ID雪花算法(Snowflake)✅ 近似✅ 是✅ 是
随机型 IDUUID, 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 是一种性能极高、架构灵活、实现简单的最佳实践方案,完美替代数据库主键自增的瓶颈问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值