基于本文回答

播面 播面

刷题像听歌,多听自然懂
0
评论

如何生成全局唯一的 ID?(雪花算法、数据库号段模式等)

知识点图片

生成全局唯一 ID(Distributed ID)是分布式系统中非常常见且关键的需求。一个优秀的分布式 ID 通常需要满足以下核心指标:

  1. 全局唯一性:不能出现重复。
  2. 趋势递增:通常用于数据库主键,利于 B+ 树索引性能(避免页分裂)。
  3. 高性能:生成 ID 的延迟要低,并发要高。
  4. 高可用:ID 生成服务不能挂,否则整个业务瘫痪。

以下是目前业界主流的几种方案及其优缺点深度解析:


1. 雪花算法 (Snowflake) —— 最主流的算法方案

Twitter 开源的算法,核心思想是利用 64位 long 型 的数字来存储 ID。

结构组成 (64 bit)

  • 1 bit:不使用(符号位,始终为0)。
  • 41 bits:毫秒级时间戳(可以使用 69 年)。
  • 10 bits:机器 ID(5位数据中心 ID + 5位工作机器 ID),支持 1024 个节点。
  • 12 bits:毫秒内的序列号,支持同一毫秒内生成 4096 个 ID。

优点

  • 高性能:本地生成,无网络消耗,单机并发极高(理论上每秒几百万)。
  • 趋势递增:基于时间戳,保证了 ID 是随时间递增的,对数据库索引友好。
  • 不依赖第三方组件:不需要依赖 Redis 或数据库。

缺点 & 痛点

  • 时钟回拨问题 (Clock Rollback):这是雪花算法最大的坑。如果服务器时间被回调(比如 NTP 校准),可能会导致 ID 重复。
    • 解决方案:记录上一次生成的时间戳,如果发现当前时间小于上一次时间,则拒绝生成(抛异常)或等待时间追平;或者启用备用 Worker ID。
  • 机器 ID 分配:在分布式容器环境(如 Kubernetes)中,如何动态分配唯一的 Worker ID 是个工程问题(通常依赖 Zookeeper/Redis 协调)。

开源实现参考

  • Baidu UidGenerator:解决了时钟回拨问题,使用 RingBuffer 缓存。
  • 美团 Leaf-snowflake:依赖 Zookeeper 管理 Worker ID。

2. 数据库号段模式 (Segment Mode) —— 最主流的 DB 方案

这是对传统数据库 auto_increment 的优化版。不再每次获取 ID 都请求数据库,而是批量获取

原理

  1. 数据库中有一张表,记录当前的 max_idstep (步长,例如 1000)。
  2. 应用服务启动或 ID 用尽时,去数据库执行 UPDATE,把 max_id 更新为 max_id + step
  3. 应用服务拿到这 1000 个 ID 的范围(例如 1000~2000),在内存中慢慢发号。
  4. 用完后,再次请求数据库获取下一个号段。

数据库表设计示例

sql
CREATE TABLE `id_generator` (
  `biz_tag` varchar(128) NOT NULL, -- 业务标识,如 order, user
  `max_id` bigint(20) NOT NULL,    -- 当前最大 ID
  `step` int(11) NOT NULL,         -- 步长
  `version` int(20) NOT NULL,      -- 乐观锁版本号
  PRIMARY KEY (`biz_tag`)
);

优点

  • 高性能:数据库压力极小(一次请求管很久)。
  • 高可用:即使数据库短时间宕机,内存中剩余的号段还能撑一阵子。
  • 可扩展:扩容方便,增加发号服务节点即可。

缺点

  • ID 不连续:如果服务重启,内存中未发完的 ID 会丢失,导致 ID 空洞(通常业务可接受)。
  • TP999 抖动:当号段用完去 DB 取新号段时,会有一次网络 IO,可能导致该次请求耗时变长。

优化:双 Buffer 机制 (美团 Leaf 核心优化)

为了解决“TP999 抖动”问题,在当前号段用到 10%~50% 时,异步线程提前去数据库把下一个号段取回来放入备用 Buffer。这样主线程切换号段时完全基于内存,无感知。


3. Redis 生成方案

利用 Redis 的原子操作 INCRINCRBY

原理

bash
INCR order_id_seq

优点

  • 性能优于数据库:Redis 基于内存,吞吐量高。
  • 有序递增:严格递增,没有空洞。

缺点

  • 运维成本:需要维护 Redis 集群的高可用。
  • 持久化风险
    • RDB 模式:Redis 宕机可能会回滚数据,导致 ID 重复。
    • AOF 模式:性能有损耗,且重启恢复慢。
  • 网络瓶颈:每次生成都需要网络请求(可以通过类似号段模式的 INCRBY 批量获取来优化)。

4. UUID (Universally Unique Identifier)

原理

基于时间、网卡 MAC 地址、随机数等生成的 128 位字符串。

优点

  • 最简单:代码一行搞定,完全本地生成,无任何依赖。
  • 全球唯一

缺点 (极度不推荐作为 MySQL 主键)

  • 无序:UUID 是无序字符串,导致 B+ 树索引频繁发生页分裂 (Page Split),写入性能极差。
  • 太长:128 位(通常存为 36 字符的 String),占用空间大,且导致二级索引变大,降低内存缓存命中率。
  • 可读性差:无法携带业务含义。

总结与选型建议

方案 优点 缺点 适用场景
UUID 简单、无依赖 无序、太长、性能差 生成 Token、文件名、链路追踪 ID(勿用于 DB 主键
数据库号段模式 高可用、高性能、趋势递增 ID 不连续、依赖 DB 绝大多数互联网业务的主键生成(推荐)
雪花算法 极高性能、不依赖 DB 强依赖时钟、时钟回拨问题 超高并发、对 ID 吞吐量要求极高的场景
Redis 实现简单、严格递增 引入中间件复杂度、持久化风险 需要严格连续递增的场景(如发票号)

工业界推荐做法:

  1. 如果不想引入复杂架构,且并发量适中:使用 数据库号段模式(手写一个简单的或使用美团 Leaf-segment)。
  2. 如果并发极高,或者不希望依赖数据库:使用 雪花算法(推荐使用百度 UidGenerator 或美团 Leaf-snowflake 这种优化过的版本,不要直接用原始算法,以免踩时钟回拨的坑)。
00:00
00:00