分表后全局 ID 如何生成?
一则或许对你有用的小广告
欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书
《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于
Spring AI + Spring Boot 3.x + JDK 21...,查看介绍《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/
新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍
截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观
面试考察点
-
问题意识:面试官首先想确认你懂不懂“为什么分表后不能用 MySQL 自增 ID”。一个表拆成 1024 张子表后,每张子表各自自增,主键必然冲突,业务上也根本没法用。如果连这个起点都说不清,后面的方案就是空中楼阁。
-
方案广度:希望你能系统列出主流的分布式 ID 方案(UUID、数据库自增步长、号段模式、Redis、雪花算法、Leaf、UidGenerator),而不是只会背一个 Snowflake。能讲多少种、能讲多深,直接反映你对分布式系统的阅读面。
-
选型与权衡能力:这才是这道题真正的核心。不同的业务场景对 ID 有不同要求——订单系统要严格递增、日志系统只要唯一就行、对外暴露的 ID 不能被反推出业务量。能不能结合“唯一性、趋势递增、可用性、性能、信息安全”这几个维度做对比和取舍,是高级候选人和初级候选人的分水岭。
核心答案
先给结论,目前业界主流的全局 ID 生成方案有 6 大类:
| 方案 | 核心思路 | 性能 | 趋势递增 | 依赖外部组件 | 适用场景 |
|---|---|---|---|---|---|
| UUID | 本地生成随机/时间戳字符串 | 极高 | 否 | 无 | 唯一性优先、不要求递增 |
| 数据库自增(步长) | 不同节点初始值不同 + 步长隔离 | 一般 | 是 | MySQL | 中小规模、表数量固定 |
| 数据库号段模式 | 批量从 DB 取一段 ID 缓存到本地 | 高 | 是 | MySQL | 中大规模、强趋势递增 |
| Redis INCR | 用 Redis 的 INCR / INCRBY |
高 | 是 | Redis | 已有 Redis 基础设施 |
| 雪花算法 Snowflake | 时间戳 + 机器号 + 序列号 | 极高 | 是 | 时钟 | 超大规模、对性能敏感 |
| 美团 Leaf / 百度 UidGenerator | 工业级封装方案 | 高 | 是 | DB/ZK | 生产环境推荐 |
生产环境真正用得多的就两种:号段模式 和 雪花算法。前者代表是美团 Leaf-Segment,后者代表是 Twitter Snowflake 及其各种变体。
深度解析
一、为什么分表后必须有全局 ID?
这块很多人理解得浅,我把话说透。
分表之前,一张订单表主键用 AUTO_INCREMENT,没问题——所有数据都在一张表里,MySQL 用一个计数器保证不重复。
分表之后,假设按用户 ID 取模拆成 16 张子表 order_0 到 order_15,每张子表内部都从 1 开始自增,那必然出现:order_0 有个 ID=1 的订单,order_1 也有个 ID=1 的订单。这两个 ID 在业务层完全没法用——比如要做跨表查询、要分页、要给前端展示、要做数据迁移合并,全都乱套。
更关键的是,对外暴露的订单号如果是个递增的小数字,竞争对手能通过“今天下一个单、明天下一个单”反推出你的业务量,这就不只是技术层面的事了。所以全局 ID 不仅要唯一,还要在很多时候不可被反推规模。
那一个合格的全局 ID,到底要满足哪些条件?我整理了这么几点:
- 全局唯一:这是底线,跨库跨表都不能重复
- 趋势递增:保证 B+ 树索引的写入性能(随机 ID 会导致页分裂频繁)
- 高性能:生成速度不能成为系统瓶颈,最好本地就能生成
- 高可用:ID 生成器挂了不能拖垮整个业务
- 信息安全(可选):不容易被外部反推出业务量
二、方案一:UUID
最简单的方案,本地 UUID.randomUUID() 直接生成。
优点:
- 完全本地生成,零网络开销,性能极高
- 不依赖任何外部组件,没有单点问题
致命缺点:
- 太长,36 个字符(含中划线),作为主键占用空间大,二级索引更夸张(MySQL 二级索引存的是主键,主键越长,所有索引都跟着膨胀)
- 完全无序,作为 InnoDB 主键会导致频繁的页分裂,写入性能断崖式下降
- 可读性差,给用户展示一个
550e8400-e29b-41d4-a716-446655440000这种订单号,体验很糟糕
我的判断:UUID 适合做 TraceId、日志关联 ID、临时文件名这些场景,但绝对不适合做分表后的主键。
三、方案二:数据库自增 + 步长
让不同的 MySQL 实例(或不同的子表)用不同的初始值和步长,错开 ID 空间。
DB1: 起始值 1, 步长 3 → 1, 4, 7, 10...
DB2: 起始值 2, 步长 3 → 2, 5, 8, 11...
DB3: 起始值 3, 步长 3 → 3, 6, 9, 12...
优点:实现简单,依赖 MySQL 自身机制,ID 严格递增。
缺点:
- 扩展性差:步长定了就难改,加节点要重新规划整个 ID 空间
- 性能瓶颈:每次生成 ID 都要写库,QPS 撑死几千
- 单点风险(除非做多主架构)
适合子表数量固定、规模可控的场景,比如内部系统的分表。大型互联网业务基本不用。
四、方案三:数据库号段模式(重点)
这是生产环境最主流的方案,美团 Leaf-Segment 就是代表。
核心思路:不要每次生成一个 ID 就去访问 DB,而是一次性取一段缓存到本地。
详细说一下双 Buffer 优化,这是 Leaf 最精髓的地方:
- Leaf 本地维护两个号段 Buffer:
current和next - 当
current已经使用了 10%(这个阈值可配)时,就异步去 DB 加载next current用完后,原子切换到next,新的next再去 DB 加载- 这样即使 DB 短暂不可用,本地还有
next号段顶一阵子,可用性就稳多了
优点:
- 性能极高,本地内存自增,单机 QPS 可达百万级
- ID 严格递增(号段内)
- DB 压力小,每次取号段相当于批量获取
- 双 Buffer 让 DB 短暂故障不影响业务
缺点:
- ID 仍然依赖 DB,DB 挂了时间长了照样完蛋
- 跨号段的 ID 不是严格连续递增(号段切换时可能有跳跃)
- 机器重启会浪费一段 ID
五、方案四:Redis INCR
利用 Redis 的 INCR 或 INCRBY 命令原子性递增。
优点:性能高、实现简单,ID 严格递增。
缺点:
- 强依赖 Redis,Redis 挂了就崩(除非用 Cluster / 哨兵保证高可用)
- Redis 持久化的问题:RDB 间隔太大可能丢一段 ID,AOF 虽然安全但性能有损耗
- 容量上 Redis 比 DB 更受限
适合规模不大、已有 Redis 基础设施的场景,不推荐作为核心业务的唯一方案。
六、方案五:雪花算法 Snowflake(重点)
Twitter 开源的 64 位整数 ID 生成算法,分布式 ID 的“顶流”。
ID 结构(64 bit):
逐段解释一下:
- 符号位(1 bit):固定为 0,保证 ID 是正数
- 时间戳(41 bit):毫秒级,可用 69 年(2^41 / 1000 / 60 / 60 / 24 / 365 ≈ 69.7 年)。一般用“当前时间 - 起始时间”的差值,起始时间叫“原始纪元”(twepoch)
- 机器号(10 bit):一般拆成 5 位数据中心 ID + 5 位机器 ID,支持 32 × 32 = 1024 个节点。分配方式可以用 ZooKeeper、数据库、配置中心
- 序列号(12 bit):同一毫秒内自增,最多 4095,意味着单机每毫秒最多生成 4096 个 ID,单机理论 QPS 可达 400 万+
优点:
- 本地生成,性能极高(微秒级)
- 趋势递增(高位是时间戳)
- 不依赖 DB / Redis,无单点
- 64 bit long 类型,存储和索引友好
缺点(必须知道的坑):
-
时钟回拨问题(最致命):机器时钟如果发生回拨(NTP 同步、人工调整),可能生成重复 ID。常见的处理方式有这么几种:
- 回拨时间短:当前线程睡眠等待回拨时长
- 回拨时间长:抛异常,或者采用历史时间方案(百度 UidGenerator 的做法)
- 启用时钟同步保护:用 ZK 或 NTP 严格管控时钟
-
机器号分配问题:1024 个机器号在容器化、Pod 频繁伸缩的环境下管理麻烦。可以用 ZK 临时节点、启动时往 DB 注册等方式。
-
生成的 ID 暴露业务量:因为高位是时间戳、低位是序列号,连续生成的 ID 实际上泄露了系统每秒的并发量。
七、方案六:工业级开源方案
美团 Leaf
Leaf 是美团开源的分布式 ID 系统,同时支持两种模式:
- Segment 模式:前面讲的号段模式,适合需要严格递增的场景(订单号、支付流水号)
- Snowflake 模式:基于 ZK 分配 workerId,解决了机器号管理和时钟回拨(通过 ZK 持久化上次时间戳)
两种模式可以共存,按业务选择,目前在各大厂用得很广。
百度 UidGenerator
基于 Snowflake 的增强版,主要解决两个问题:
- workerId 分配:基于数据库自增 ID 分配,启动时插入一条记录拿 ID
- 时间戳使用秒级 + RingBuffer:用秒级时间戳,配合环形缓冲区预生成 ID,单机 QPS 可达 600 万+
核心创新是 RingBuffer:传统 Snowflake 是实时计算,UidGenerator 是后台线程预生成一批 ID 放到 RingBuffer 里,业务线程直接取,性能直接拉满。
需要注意,UidGenerator 默认用秒级时间戳,时间戳位数短,可用年限会比原版 Snowflake 的 69 年要短,这也是它为了 RingBuffer 性能做的一个取舍。
八、生产环境怎么选?
直接给结论:
| 业务场景 | 推荐方案 |
|---|---|
| 订单号、支付流水号(严格递增、对 DB 友好) | 美团 Leaf-Segment |
| 高并发日志、埋点、消息 ID | Snowflake / UidGenerator |
| 内部系统、规模可控 | 步长方案 |
| 已有 Redis 基础设施、规模不大 | Redis INCR |
| 对外暴露且怕反推业务量 | Snowflake + Hash 或 自定义混淆算法 |
我个人在生产中最常用的组合:对外业务用 Leaf-Segment(订单号友好),内部 ID 用 Snowflake(高性能、无依赖)。两套并行,按业务特点选。
面试高频追问
-
Snowflake 时钟回拨怎么解决?
三种思路:短回拨睡眠等待;长回拨抛异常或用历史时间方案;用 ZK 持久化上次时间戳做校验。美团 Leaf-Snowflake 就是这么做的。
-
号段模式 DB 挂了怎么办?
靠双 Buffer 顶一阵子,业务服务器本地还有
next号段可用。同时 DB 要做主从切换保证可用性。Leaf 还支持从机读取号段兜底。 -
Snowflake 的 workerId 怎么分配?
几种方式:ZK 临时节点(Leaf-Snowflake 的做法)、DB 启动注册(UidGenerator)、配置中心下发、K8s 的 StatefulSet 固定 Pod 序号。
-
ID 要不要暴露业务量?怎么避免?
要避免。Snowflake 直接暴露了并发量。可以对生成的 ID 做位混淆、Hash 散列、或者前后加随机位做混淆。订单号对外常用“业务前缀 + 时间 + Snowflake + 校验位”的混合方案。
-
能不能用 MySQL 自增 ID + 分库分表?
可以但很弱。多写主从架构(Multiple Master)让每个主库步长不同,但扩展性、性能都受限,实际很少用。
常见面试变体
- “说说分布式 ID 的几种实现方案?”
- “Snowflake 算法的原理?时钟回拨怎么处理?”
- “美团 Leaf 你了解吗?它解决了什么问题?”
- “号段模式是怎么工作的?为什么需要双 Buffer?”
- “百万 QPS 的业务怎么设计 ID 生成方案?”
记忆口诀
六大方案:UUID 杂、步长分、号段批、Redis 数、雪花位、Leaf 全。
Snowflake 结构:1 + 41 + 10 + 12 = 64 位(符号 + 时间 + 机器 + 序列)。
生产首选:递增业务选号段,高并发业务选雪花。
总结
一句话:分表后必须用全局 ID,UUID 不适合做主键,生产环境主要在号段模式(Leaf-Segment)和雪花算法(Snowflake / UidGenerator)之间选。前者适合要严格递增的订单号场景,后者适合高并发的内部 ID 场景。面试时把“为什么需要全局 ID → 6 大方案 → 重点讲号段和雪花 → 对比选型”这条线讲清楚,基本就是高分回答。
