为什么需要分布式 ID?一文搞懂分布式 ID 生成方案
一、为什么需要分布式 ID?
在单库单表中,主键 ID 通常由数据库自增特性 AUTO_INCREMENT 来生成,简单可靠,无需关心唯一性。
但在分库分表的场景下(拿用户表来说),就会导致一个问题:如果用户 ID 继续使用自增主键,由于每个分片(库/表)的计数器是独立的,就会产生大量重复的用户 ID,直接破坏全局唯一性。
因此,分布式环境下必须有一套跨库、跨服务、全局唯一的 ID 生成机制,这就是分布式 ID。
二、什么是分布式 ID?
分布式 ID 是指在一个分布式系统中,为每一个数据项或事件生成一个全局唯一标识符的过程。这个标识符通常是一个长整型数字或字符串,能够跨多个服务实例和数据库集群唯一识别每一个实体,是实现数据关联和跟踪的基础。
在传统单体应用中,ID 生成相对简单,通过数据库自增字段即可实现。但在微服务架构下,每个服务可能运行在不同的服务器上,甚至可能有多个实例,这就意味着每个服务都需要独立生成 ID,并且保证全局唯一性。
此外,分布式 ID 还需要解决以下几个关键问题:
- 一致性:所有生成的 ID 必须在分布式环境中保持一致,避免重复和冲突;
- 高性能:在高并发场景下,ID 生成机制不能成为系统的瓶颈;
- 可扩展性:随着业务增长,ID 生成策略应该易于扩展,适应更大的负载;
- 容错性:即使部分服务出现故障,ID 生成也不能中断。
三、常见分布式 ID 生成方案
1. UUID
UUID(Universally Unique Identifier)是一种常用的分布式 ID 生成方式。标准型式包含 32 个 16 进制数字,以连字号分为五段,形式为 8-4-4-4-12 的 36 个字符,示例:550e8400-e29b-41d4-a716-446655440000。
优点:
- 生成性能非常高:直接本地生成,不依赖其他中间件,无网络 / 磁盘 IO 消耗。
缺点:
- 不易于存储:UUID 太长,16 字节 128 位,通常以 36 长度字符串表示。海量数据场景下会消耗较大存储空间;
- 信息不安全:基于 MAC 地址生成 UUID 的算法可能造成 MAC 地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置;
- 作为 MySQL 主键不合适:MySQL 官方明确建议主键尽量越短越好,36 字符长度的 UUID 不符合要求;
- 对 MySQL 索引不利:以 InnoDB 引擎为例,B+ 树按主键从小到大排列数据,每次写入新数据会被顺序添加到数据页尾部。而 UUID 无序,新数据可能被插入数据页中间,B+ 树不得不把已有数据后移,数据位置频繁变动,严重影响性能。
2. 基于数据库(DB)的自增 ID
可以单独创建一张共享的 ID 生成表,使用自增字段生成 ID,再写入业务表主键。
优点:
- 实现非常简单,利用现有数据库即可;
- ID 单调递增。
缺点:
- 强依赖 DB,DB 异常时整个系统不可用,属致命问题。配置主从复制可提升可用性,但主从切换时的不一致可能导致重复发号;
- 发号性能受限于单台 MySQL 的读写性能。
3. 基于分布式协调服务
利用 Zookeeper、Etcd 等分布式协调服务,可以实现 ID 的有序分配。可以保证 ID 的顺序性,但引入了外部依赖,增加了系统复杂度。
4. 基于分布式缓存(Redis)
使用 Redis 的 INCRBY 命令,为某个 Key 的数字增加指定增量;Key 不存在时先初始化为 0 再执行增量。利用 Redis 单线程原子性保证并发下的唯一与递增,适合对趋势递增有要求的场景。
5. 基于 Snowflake 雪花算法
Snowflake 算法由 Twitter 开发,结合时间戳、机器 ID 和序列号,生成 64 位的 ID,位结构如下:
- 1 bit 符号位:标识正负,始终为 0,代表生成的 ID 为正数;
- 41 bit 时间戳:单位毫秒,可支撑 2^41 毫秒(约 69 年);
- 10 bit 机器 ID:一般前 5 位表示机房 ID,后 5 位表示机器 ID(可按需调整),用于区分不同集群 / 机房的节点;
- 12 bit 序列号:自增值,代表单台机器每毫秒可生成的最大 ID 数(2^12 = 4096),即单机每毫秒最多生成 4096 个唯一 ID。理论上 snowflake 方案 QPS 约 409.6 万 /s。
优点:
- 毫秒数在高位、自增序列在低位,整个 ID 趋势递增;
- 不依赖数据库等第三方系统,以服务方式部署,稳定性更高,生成性能非常高;
- 可根据自身业务特性分配 bit 位,非常灵活。
缺点:
- 强依赖机器时钟,如果机器时钟回拨,会导致发号重复或服务不可用。
四、业界开源方案
百度 UidGenerator
- GitHub:
https://github.com/baidu/uid-generator - 基于 Snowflake 算法的 Java 实现,以组件形式工作在应用项目中,支持自定义 workerId 位数和初始化策略,适用于 docker 等虚拟化环境下实例自动重启、漂移场景。
- 通过借用未来时间解决 sequence 的并发限制;采用 RingBuffer 缓存已生成的 UID,并行化生产与消费,并对 CacheLine 补齐,避免 RingBuffer 的「伪共享」问题,单机 QPS 可达 600 万。
滴滴 TinyID
- GitHub:
https://github.com/didi/tinyid/wiki - 基于数据库号段算法的 Java 分布式 ID 生成系统,扩展了 leaf-segment 算法,支持多 db(master),并提供 java-client(sdk) 使 ID 生成本地化,性能与可用性更好。滴滴客服部门均通过 tinyid-client 接入,每天生成亿级别 ID。
美团 Leaf
- GitHub:
https://github.com/Meituan-Dianping/Leaf - 早期需求来自各业务线的订单 ID 生成,早期业务混用 DB 自增、Redis、UUID 等方式,各自有问题,于是实现统一分布式 ID 生成服务。覆盖美团金融、餐饮、外卖、酒店旅游、猫眼电影等业务线。在 4C8G VM 上通过 RPC 调用,QPS 近 5w/s,TP999 1ms。
- 设计文档见《Leaf 美团分布式ID生成服务》,支持号段模式(leaf-segment)与雪花模式(leaf-snowflake)双引擎。
腾讯微信 Seqsvr
- 未开源,可参考《微信序列号生成器架构设计及演变》一文。
五、方案对比与选择建议
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| UUID | 本地生成快、无依赖 | 长、无序、不利索引、占存储 | 非数据库主键的标识 |
| 数据库自增 | 简单、单调递增 | 强依赖 DB、性能瓶颈 | 低并发、单库 |
| 协调服务(ZK/Etcd) | 有序、可控 | 引入外部依赖、复杂度高 | 需要强一致发号 |
| Redis INCRBY | 简单、趋势递增 | 依赖 Redis、需要持久化保障 | 中低并发、允许少量依赖 |
| Snowflake | 高性能、趋势递增、可扩展 | 依赖时钟、需处理回拨 | 绝大多数高并发场景 |
| Leaf / TinyID / UidGenerator | 生产级、成熟 | 需部署中间件/接入 SDK | 大型分布式系统 |
选型建议:
- 中小项目追求简单:直接选 Snowflake 雪花算法或成熟开源组件(Leaf 号段/雪花、TinyID);
- 对趋势递增和排序有强要求:优先 号段模式(Leaf/TinyID);
- 数据量巨大且时钟不可控(云上):考虑 UidGenerator 等对时钟回拨有更强容忍的实现;
- 绝大多数场景,不要从零造轮子,直接选用上述生产级开源方案。
六、总结
- 单库单表靠
AUTO_INCREMENT即可,分库分表后才必须引入分布式 ID; - 分布式 ID 要同时满足:全局唯一、高性能、可扩展、高容错;
- 没有银弹:UUID 简单但无序,DB 自增可靠但有单点,雪花算法高性能但依赖时钟;
- 生产环境优先选择 百度 UidGenerator、滴滴 TinyID、美团 Leaf 等久经考验的方案,别重复造轮子;
- 选型核心权衡:性能 vs 复杂度 vs 有序性 vs 可用性。