分表后全局 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+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 问题意识:面试官首先想确认你懂不懂“为什么分表后不能用 MySQL 自增 ID”。一个表拆成 1024 张子表后,每张子表各自自增,主键必然冲突,业务上也根本没法用。如果连这个起点都说不清,后面的方案就是空中楼阁。

  2. 方案广度:希望你能系统列出主流的分布式 ID 方案(UUID、数据库自增步长、号段模式、Redis、雪花算法、Leaf、UidGenerator),而不是只会背一个 Snowflake。能讲多少种、能讲多深,直接反映你对分布式系统的阅读面。

  3. 选型与权衡能力:这才是这道题真正的核心。不同的业务场景对 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_0order_15,每张子表内部都从 1 开始自增,那必然出现:order_0 有个 ID=1 的订单,order_1 也有个 ID=1 的订单。这两个 ID 在业务层完全没法用——比如要做跨表查询、要分页、要给前端展示、要做数据迁移合并,全都乱套。

分表 ID 冲突
分表 ID 冲突

分表全局 ID
分表全局 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
号段双 Buffer

详细说一下双 Buffer 优化,这是 Leaf 最精髓的地方:

  • Leaf 本地维护两个号段 Buffer:currentnext
  • current 已经使用了 10%(这个阈值可配)时,就异步去 DB 加载 next
  • current 用完后,原子切换到 next,新的 next 再去 DB 加载
  • 这样即使 DB 短暂不可用,本地还有 next 号段顶一阵子,可用性就稳多了

优点

  • 性能极高,本地内存自增,单机 QPS 可达百万级
  • ID 严格递增(号段内)
  • DB 压力小,每次取号段相当于批量获取
  • 双 Buffer 让 DB 短暂故障不影响业务

缺点

  • ID 仍然依赖 DB,DB 挂了时间长了照样完蛋
  • 跨号段的 ID 不是严格连续递增(号段切换时可能有跳跃)
  • 机器重启会浪费一段 ID

五、方案四:Redis INCR

利用 Redis 的 INCRINCRBY 命令原子性递增。

优点:性能高、实现简单,ID 严格递增。

缺点

  • 强依赖 Redis,Redis 挂了就崩(除非用 Cluster / 哨兵保证高可用)
  • Redis 持久化的问题:RDB 间隔太大可能丢一段 ID,AOF 虽然安全但性能有损耗
  • 容量上 Redis 比 DB 更受限

适合规模不大、已有 Redis 基础设施的场景,不推荐作为核心业务的唯一方案。

六、方案五:雪花算法 Snowflake(重点)

Twitter 开源的 64 位整数 ID 生成算法,分布式 ID 的“顶流”。

ID 结构(64 bit)

Snowflake ID 结构
Snowflake ID 结构

雪花算法 ID
雪花算法 ID

逐段解释一下:

  • 符号位(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 类型,存储和索引友好

缺点(必须知道的坑)

  1. 时钟回拨问题(最致命):机器时钟如果发生回拨(NTP 同步、人工调整),可能生成重复 ID。常见的处理方式有这么几种:

    • 回拨时间短:当前线程睡眠等待回拨时长
    • 回拨时间长:抛异常,或者采用历史时间方案(百度 UidGenerator 的做法)
    • 启用时钟同步保护:用 ZK 或 NTP 严格管控时钟
  2. 机器号分配问题:1024 个机器号在容器化、Pod 频繁伸缩的环境下管理麻烦。可以用 ZK 临时节点、启动时往 DB 注册等方式。

  3. 生成的 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(高性能、无依赖)。两套并行,按业务特点选。

面试高频追问

  1. Snowflake 时钟回拨怎么解决?

    三种思路:短回拨睡眠等待;长回拨抛异常或用历史时间方案;用 ZK 持久化上次时间戳做校验。美团 Leaf-Snowflake 就是这么做的。

  2. 号段模式 DB 挂了怎么办?

    靠双 Buffer 顶一阵子,业务服务器本地还有 next 号段可用。同时 DB 要做主从切换保证可用性。Leaf 还支持从机读取号段兜底。

  3. Snowflake 的 workerId 怎么分配?

    几种方式:ZK 临时节点(Leaf-Snowflake 的做法)、DB 启动注册(UidGenerator)、配置中心下发、K8s 的 StatefulSet 固定 Pod 序号。

  4. ID 要不要暴露业务量?怎么避免?

    要避免。Snowflake 直接暴露了并发量。可以对生成的 ID 做位混淆、Hash 散列、或者前后加随机位做混淆。订单号对外常用“业务前缀 + 时间 + Snowflake + 校验位”的混合方案。

  5. 能不能用 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 大方案 → 重点讲号段和雪花 → 对比选型”这条线讲清楚,基本就是高分回答。