深入浅出分布式 ID 生成方案:从原理到业界主流实现

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在复杂的分布式系统中,随着业务数据量的爆发式增长,传统的单库单表架构往往难以支撑,分库分表成为了常见的扩展手段。而在分库分表的环境下,如何生成一个全局唯一、高性能且高可用的 ID,成为了架构设计中必须解决的核心问题。

本文将探讨分布式 ID 的由来、核心要求,并详细解析业界主流的开源解决方案:百度的 UidGenerator、滴滴的 TinyID,以及重点剖析美团的 Leaf 系统。


为什么需要分布式 ID?

在单体应用或单库单表中,我们通常使用数据库(如 MySQL)的自增主键(AUTO_INCREMENT)来生成 ID。这种方式简单、可靠,且生成的 ID 有序,对数据库索引友好。

然而,在分库分表的场景下,单纯依赖数据库自增就会遇到问题:

  • ID 重复:不同分片上的数据库各自自增,会导致 ID 冲突。
  • 扩展困难:无法通过简单的扩容来解决 ID 唯一性问题。

此外,在微服务架构中,订单号、优惠券码、Trace ID 等都需要全局唯一标识。

分布式 ID 的核心要求

一个优秀的分布式 ID 生成系统,通常需要满足以下指标:

  1. 全局唯一性:这是最基本的要求,不能出现重复 ID。
  2. 高性能:生成 ID 的延迟要低,QPS 要高,不能成为系统的瓶颈。
  3. 高可用:ID 生成服务必须极其稳定,一旦宕机可能导致整个业务瘫痪(如无法下单)。
  4. 趋势递增:最好是趋势递增的(不一定严格连续),这对数据库索引(如 B+ 树)的写入性能非常重要。
  5. 安全性:如果 ID 严格连续(如 1, 2, 3…),容易被恶意爬取或推测业务量,因此在某些场景下需要 ID 无规则。

常见的基础方案

在介绍大厂方案前,先回顾几种基础方案:

  • UUID
    • 优点:本地生成,性能极高,无网络消耗。
    • 缺点:太长(128位,36字符),无序,作为 DB 主键会导致 B+ 树频繁分裂,写入性能差。
  • 数据库自增(多主模式)
    • 利用 MySQL 的 auto_increment_incrementauto_increment_offset 设置步长。
    • 缺点:扩展性差,强依赖数据库,单点压力大。
  • Snowflake(雪花算法)
    • Twitter 开源,由 1bit 符号位 + 41bit 时间戳 + 10bit 机器 ID + 12bit 序列号组成。
    • 优点:不依赖数据库,高性能,有序。
    • 缺点:强依赖机器时钟,时钟回拨会导致 ID 重复或服务不可用。

业界主流开源方案

各大互联网公司基于上述基础方案,针对自身的业务场景和痛点,研发并开源了成熟的分布式 ID 系统。

1. 百度 UidGenerator

UidGenerator 是百度开源的 Java 语言实现的唯一 ID 生成器,基于 Snowflake 算法改进。

核心特点:

  • RingBuffer 缓存:与原生 Snowflake 不同,UidGenerator 采用了双 RingBuffer 结构来缓存 ID,采用了生产者-消费者模型。
    • 一个 RingBuffer 用于存储生成的 ID。
    • 一个 RingBuffer 用于存储状态(Flag)。
    • 这种方式使得 ID 的获取操作变成了纯内存读取,性能极高(QPS 可达 600万+)。
  • 支持自定义 bit 分配:默认的时间戳位较少(28位),支持约 8.7 年,但可以通过配置调整。
  • 解决时钟回拨:通过借用未来时间来解决时钟回拨问题(因为是预生成的)。

适用场景:对性能要求极高,且并发量巨大的场景。

2. 滴滴 TinyID

TinyID 是滴滴开源的分布式 ID 生成系统,它是基于数据库号段模式(Segment)实现的。

核心特点:

  • 号段模式:不每次去数据库取 ID,而是每次取一个“号段”(例如 1000 个 ID),加载到内存中慢慢发,用完了再去取。
  • 多 DB 支持:支持多 DB 源,提高了可用性。
  • TinyID Client(SDK):提供了 Java 客户端。
    • 双号段缓存:客户端也会在内存中缓存双号段(当前使用的和下一个备用的)。
    • 这意味着即使 TinyID Server 挂了,客户端在一段时间内(号段用完前)还能继续生成 ID,极大地提高了容灾能力。
  • HTTP 方式:也支持 HTTP 请求获取 ID,方便非 Java 语言接入。

适用场景:对数据库依赖较高,但希望架构简单、可用性强的场景。TinyID 只支持号段模式,不支持 Snowflake 模式。


3. 美团 Leaf (详细解析)

Leaf 是美团点评开源的分布式 ID 生成系统,它不仅解决了上述方案的许多问题,还提供了极高的稳定性和灵活性。Leaf 这个名字寓意“世界上没有两片完全相同的树叶”。

Leaf 提供了两种生成模式:Leaf-segment(号段模式)Leaf-snowflake(雪花模式)

原文章地址链接https://tech.meituan.com/2017/04/21/mt-leaf.html

3.1 Leaf-segment 模式(号段模式)

Leaf-segment 模式是对数据库自增方案的优化和升级,核心思想是“批量获取”,将数据库的压力降到最低。

1. 数据库设计
Leaf 使用一张表 leaf_alloc 来维护不同业务的 ID 发号进度:

CREATE TABLE `leaf_alloc` (
  `biz_tag` varchar(128)  NOT NULL DEFAULT '', -- 业务标识,如 order, user
  `max_id` bigint(20) NOT NULL DEFAULT '1',    -- 当前已分配的最大 ID
  `step` int(11) NOT NULL,                     -- 步长,每次取号段的长度
  `description` varchar(256)  DEFAULT NULL,    -- 描述
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB;

2. 获取号段流程
每次 Leaf 服务去数据库取号段时,执行如下事务操作:

UPDATE leaf_alloc SET max_id = max_id + step WHERE biz_tag = #{tag}
SELECT max_id, step FROM leaf_alloc WHERE biz_tag = #{tag}

这样 Leaf 服务就拿到了 [max_id - step, max_id] 这个范围的 ID(号段),可以在内存中慢慢分配,直到用完。

3. 双 Buffer 优化(核心创新)
简单的号段模式有一个问题:当号段用完时,需要同步去 DB 取下一个号段,这会导致瞬间的线程阻塞和 TP999 飙升。Leaf 采用了 双 Buffer 机制来解决这个问题:

  • 架构:Leaf 服务内部有两个号段缓冲区(Buffer A 和 Buffer B)。
  • 流程
    1. 当前服务正在消耗 Buffer A 中的 ID。
    2. 当 Buffer A 中的 ID 消耗达到一定阈值(如 10% 或 20%)时,后台线程会异步去数据库获取下一个号段,并填充到 Buffer B 中。
    3. 当 Buffer A 彻底用完时,直接瞬间切换到 Buffer B 继续发号,同时 Buffer A 变成备用,后台线程再次去准备下一个号段填充 Buffer A。
  • 效果:除了服务启动时的第一次获取,后续所有的 DB IO 操作都是异步的,获取 ID 的操作变成了纯内存操作,极大地提高了性能和稳定性。

优点

  • 高性能:TP999 极低,ID 生成完全在内存中。
  • 高可用:即使数据库宕机,只要内存中的号段还没用完,Leaf 依然可以坚持一段时间(取决于 step 大小)。
  • 方便扩容:增加 Leaf 服务节点非常简单,无需分库分表。

缺点

  • ID 不随机:ID 是趋势递增的,竞争对手可能推测出业务量(可以通过混淆 ID 解决)。
  • 依赖 DB:虽然有缓存,但最终还是依赖数据库的稳定性。
3.2 Leaf-snowflake 模式(雪花模式)

对于需要 ID 具有时间属性,或者完全不依赖数据库的场景,Leaf 提供了 Snowflake 模式。

1. ID 结构
结构遵循标准的 Snowflake,但对 ZooKeeper 进行了强绑定:

  • 1bit:符号位,始终为 0。
  • 41bit:时间戳(毫秒级),支持约 69 年。
  • 10bit:WorkID(机器 ID),支持 1024 个节点。
  • 12bit:序列号,同一毫秒内支持 4096 个 ID。

2. ZooKeeper 管理 WorkID
Leaf 使用 ZooKeeper 持久顺序节点来自动分配 WorkID:

  1. 启动注册:Leaf 服务启动时,去 ZK 的 /snowflake/${leaf.name}/service/address 下创建一个持久顺序节点
  2. 获取 ID:ZK 返回生成的顺序 ID,Leaf 将其解析为 WorkID(0-1023)。
  3. 本地缓存:Leaf 会将获取到的 WorkID 缓存到本地文件(leaf.properties)。这样即使 ZK 挂了,服务重启时也能读取本地文件恢复 WorkID,属于弱依赖 ZK

3. 解决时钟回拨(三道防线)
Snowflake 最大的问题是时钟回拨,Leaf 做了周全的防御:

  1. 启动校验:启动时,检查本机时间是否小于 ZK 上该节点最后上报的时间。如果是,说明时钟回拨,服务拒绝启动并报警。
  2. 运行时校验:服务运行中,每隔 3 秒向 ZK 上报当前时间。如果发现本机时间小于上次上报时间,说明回拨,暂停服务。
  3. 备用容错:如果 ZK 挂了,会对比本机时间与本地缓存文件中记录的时间。

优点

  • 高性能:无 DB 依赖,纯内存生成。
  • 时间有序:ID 包含时间信息,便于排序和统计。
  • 弱依赖 ZK:ZK 短暂故障不影响核心发号服务。

缺点

  • 依赖 ZK:需要维护 ZooKeeper 集群。
  • 时钟敏感:虽然有防御机制,但系统时间必须精确。

总结

方案核心算法依赖组件优点缺点
百度 UidGeneratorSnowflakeMySQL + RingBuffer性能极高(内存生产),解决回拨ID 位数配置较复杂,维护 RingBuffer 稍繁琐
滴滴 TinyIDSegmentMySQL客户端缓存容灾强,架构简单不支持 Snowflake 模式
美团 LeafSegment + SnowflakeMySQL / ZooKeeper双模式灵活切换,双 Buffer 优化非常稳定需维护 ZK 或 DB

在实际选型中:

  • 如果希望架构简单,对 ID 连续性要求不高,Leaf-segmentTinyID 是很好的选择。
  • 如果需要 ID 包含时间信息,且并发量巨大,Leaf-snowflakeUidGenerator 更合适。
  • 美团 Leaf 由于提供了两种模式的平滑切换和优秀的双 Buffer 设计,是目前业界使用非常广泛的方案。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值