美团Leaf分布式ID生成系统

一、为什么需要分布式 ID

1.1 背景

在单体应用、单库单表阶段,很多业务表直接使用数据库自增主键就可以满足需求,但当系统逐步演进到以下场景后,传统自增主键方案往往就不够用了:

  • 服务拆分,系统进入分布式架构

  • 数据量增长,开始分库分表

  • 多机房、多实例部署

  • 业务希望主键同时承担一定的排序能力

  • 对外暴露的 ID 不希望泄露业务规模和数据安全

这时候,业务通常需要一套全局唯一的****分布式 ID 生成方案,用于订单、用户、支付、日志、流水等核心数据的主键或业务标识。

1.2 分布式 ID 的核心诉求

在实际业务中,分布式 ID 往往不只是 “唯一” 这么简单,通常还会同时关注以下几个维度:

1)全局唯一 这是最基础的要求,任何两个不同的数据对象都不能拿到相同的 ID。

2)有序性 有序性通常分为两类:

  • 严格单调递增:ID 严格按 1、2、3、4 这样递增,这种形式最理想,但在分布式多节点场景下实现成本较高。

  • 趋势递增:整体随时间变大,局部允许乱序,这是分布式场景里更常见的目标,比如 Snowflake 方案。

对于 MySQL InnoDB 这类使用聚簇索引的数据库来说,主键越接近有序,通常越有利于写入性能和索引维护。

3)高性能 要求在高并发场景下仍能稳定生成 ID,具备较高 QPS 和较低延迟。

4)高可用 ID 服务不能成为业务单点。一旦ID生成系统不可用,往往会直接影响下游订单、库存、支付等链路。

5)安全性 如果 ID 具有非常明显的连续规律,外部很容易通过 ID 推测业务量、订单量、用户增长规模,这在很多对外业务中是不可接受的;同时连续 ID 容易被恶意遍历爬取数据,存在数据泄露的安全风险。

6)易运维 分布式 ID 方案往往涉及机器号、时钟、协调组件、数据库等问题,如果运维复杂度过高,长期维护成本会很高。

1.3 为什么没有一种方案能同时完美满足所有诉求

分布式 ID 方案本质上是在多个目标之间做权衡:唯一性、递增性、安全性、性能、可用性、运维复杂度。 例如:

  • 严格递增通常意味着中心化协调更多;

  • 完全本地生成虽然性能高,但可能带来时钟、机器号分配等问题;

  • 安全性强的方案不一定适合做数据库聚簇主键;

  • 依赖越少,可用性通常越高,但适用性可能越受限。

所以,分布式 ID 选型的关键不是 “找一个万能方案”,而是根据业务约束选择最合适的方案。


二、常见分布式 ID 方案及其短板

2.1 UUID

UUID是通用唯一识别码(Universally Unique Identifier)的缩写。它通过一定的算法机器生成,包括网卡MAC地址、时间戳、随机或伪随机数、时序等元素,UUID的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符。

UUID uuid = UUID.randomUUID();
uuid:9fea8ab3-0789-4719-aa70-8849cbc59916
  • 优点:本地生成、无网络依赖、实现简单、可用性极高、无单点问题

  • 缺点:

    • 无序:因为生成无规律,大量的随机添加势必导致MySQL底层大量的B+ tree的节点分裂,耗费大量计算资源,严重影响数据库性能

      InnoDB 主键使用的是聚簇索引,这意味着数据在物理存储上是按照主键的顺序依次排列的。如果主键是自增有序的,每次插入新数据,数据库只需在当前数据页的末尾“追加”即可,效率极高。但如果主键是无序的随机 UUID,数据库在插入新数据时,往往需要将其强行塞入到之前已经写满的数据页中间。为了腾出空间,数据库不得不把满页的数据拆分成两个页,这就是页分裂(Page Split)。大量的页分裂会导致频繁的磁盘数据移动、产生大量内存碎片,并极大地降低插入(Insert)的并发性能。

    • 字符串:存储占用空间大,查询、排序效率低

      B+ 树节点容量降低: 数据库(如 MySQL InnoDB)的主键索引使用的是 B+ 树,其每个节点(页)的大小是固定的(默认 16KB)。主键的体积越大,一个节点能容纳的主键数量就越少。这会导致 B+ 树的层级变深,查询时需要的磁盘 I/O 次数增加,直接拖慢查询速度。

      二级索引体积膨胀: 在 InnoDB 中,辅助索引(非主键索引)的叶子节点存储的是主键的值。这意味着,如果你有一个 36 字节的 UUID 作为主键,那么你表里的每一个二级索引都会多出极大的存储开销。表越大、索引越多,浪费的磁盘和内存空间就越惊人。相比之下,BIGINT 只需要 8 个字节。

      比较效率极低:在数据库执行查询、排序或多表 Join 时,底层需要频繁比对主键的值。CPU 对整型(如 BIGINT)的比较是原生的指令,速度极快;而对字符串的比较,需要逐个字符进行查对。

适用场景:对入库性能不敏感、不做主键,仅做业务标识;

不适用:高并发 MySQL 主表主键、索引敏感的核心表。

2.2 数据库自增ID

利用 MySQL auto_increment 特性,单表自增生成ID,我们也可以专门使用一张数据表生成自增唯一且有序的分布式ID:

CREATE TABLE id_allocation (
  id INT AUTO_INCREMENT PRIMARY KEY
);

  • 优点:实现简单、ID 严格递增天然唯一

  • 缺点:

    1)单点瓶颈:单实例 DB 成为性能与故障瓶颈;

    2)扩展性差:分库分表、多机房无法复用;

    3)ID 连续,容易推算业务体量,安全性差。

适用场景:小型单体、单库单表、对内非敏感业务。

2.3 多库步长自增

针对上述问题,可以使用采用「起始偏移 + 固定步长」的多库自增方案,来提高多库多表生成分布式ID以保证高可用,例如可以使用多主模式,每个库分别存放一张id分配表(id_allocation )或者分表,表1的id从1开始,表2的id从2开始,表3的id从3开始,各自的步长都为3

-- ID分配表1,从1开始自增,步长为3
CREATE TABLE db1.id_allocation (
  id INT AUTO_INCREMENT PRIMARY KEY
) AUTO_INCREMENT=1;

-- ID分配表2,从2开始自增,步长为3
CREATE TABLE db2.id_allocation (
  id INT AUTO_INCREMENT PRIMARY KEY
) AUTO_INCREMENT=2;

-- ID分配表3,从3开始自增,步长为3
CREATE TABLE db3.id_allocation (
  id INT AUTO_INCREMENT PRIMARY KEY
) AUTO_INCREMENT=3;

  • 优点:解决单库单点,性能问题,ID 整体有序;

  • 缺点:

    • 不利于后续的扩展,比较复杂,维护成本较高

    • 单个数据库压力还是很大,无法满足高并发需求

2.4 数据库号段模式

号段式分布式ID生成是当下比较主流的实现方案之一,它的整体思路是在专门建立一张数据表划分当前业务的ID分配段,每次加载一批号段到内存中,然后所有的服务都从这个号码段中获取ID,直至这个号码段用完,然后再到数据库中再划动一批到内存中使用

CREATE TABLE id_allocation (
  id int(10) NOT NULL,
  max_id bigint(20) NOT NULL COMMENT '当前已用最大id',
  step int(20) NOT NULL COMMENT '号段分配步长',
  biz_type    int(20) NOT NULL COMMENT '业务类型',
  version int(20) NOT NULL COMMENT '版本号',
  PRIMARY KEY (`id`)
) 
update id_generator set max_id = #{max_id+step}, version = version + 1 where version = # {version} and biz_type = XXX

  • 优点:减少 DB 访问频次,内存发号吞吐高;DB 临时故障,存量号段可继续发号。

  • 缺点:

    • ID 连续,泄露业务量;

    • 号段耗尽同步拉取 DB,请求阻塞,TP99 延迟毛刺;

    • 底层依赖 MySQL,数据库宕机无新号段可用。

2.5 Redis 自增

Redis分布式ID实现主要是通过提供像INCR 和 INCRBY 这样的自增原子命令,由于Redis单线程的特点,可以保证ID的唯一性和有序性

  • 优点:性能高、实现简单、ID有序

  • 缺点:

    • 依赖Redis,集群/主从切换存在一致性风险

    • 高并发下Redis仍有网络开销与压力,若Redis有阻塞命令,影响可用性

2.6 原生雪花算法(Snowflake)

雪花算法是twitter开发是一中id生成算法,将64位二进制拆分(时间戳+机器ID+序列号),本地计算生成ID。

  • 优点:

    • 在毫秒级情况下可以生成大量id,纯内存生成性能出色,毫秒数在高位,自增序列在低位趋势递增,类型也是long类型,且包含日期和workerid等具备基因特性的因子,可以满足大部分业务场景的需求

    • 不依赖数据库,分布式扩展能力强

    • 可以根据自身业务特性分配bit位,非常灵活(百度的uid-generator)

  • 缺点:

    • WorkerID: 人工配置,集群扩缩容易重复,ID 冲突风险

    • 时钟漂移:强依赖服务器时钟,时间漂移风险大

    计算机的时间并非凭空而来,而是靠主板上的石英晶体振荡器和纽扣电池来模拟的。通常这块晶体的振动频率为 32,768Hz(即每振动 32,768 次代表 1 秒过去)。 但物理硬件并不完美,受极端环境(如低温)影响,晶体振动会出现误差。在正常情况下,服务器每天也会产生±1s左右的计时误差,这种现象被称为“时钟漂移”。

    • 时钟回拨:雪花算法有41位是时间戳组成,NTP 时间同步可能引发时钟回拨,同 WorkerID + 同时间戳生成重复 ID

    既然单台机器的时间会飘,集群环境怎么保证时间一致?1985 年,David L. Mills 设计了NTP(网络时间协议****),它可以将局域网内的机器时钟误差强行拉平到 1ms 以内。 但 NTP 的介入带来了一个极其恐怖的副作用:如果某台机器的时钟跑得太快了,NTP 会把它的时间往回拨!


三、Leaf整体设计思路

Leaf 是美团开源分布式 ID 框架:

Leaf 提供两套独立实现模式,两套模式可并行部署,按需选用:

  1. Leaf-segment:优化版数据库号段,主打严格自增、无时钟依赖;

  2. Leaf-snowflake:增强版雪花,主打有序安全、趋势递增。

3.1 Leaf-segment:数据库号段模式的工程化增强

3.1.1 核心原理

基于预分配号段 + 本地内存缓存,批量从数据库获取ID区间,业务请求直接从内存发号,号段消耗至阈值,后台异步预加载下一个号段,大幅降低DB压力。

3.1.2 数据库表设计

CREATE TABLE leaf_alloc (
    biz_tag VARCHAR(128) NOT NULL COMMENT '业务标识',
    max_id BIGINT(20) NOT NULL DEFAULT 1 COMMENT '当前最大已分配ID',
    step INT(11) NOT NULL COMMENT '号段步长',
    desc VARCHAR(256) DEFAULT NULL COMMENT '业务描述',
    update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (biz_tag)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

biz_tag:业务隔离;max_id:已分配上限;step:单次申领数量。

3.1.3 三大核心优化点

  1. 双 Buffer,消除号段切换阻塞

设计主、备两个号段容器;当当前号段消耗90%水位线,异步线程提前拉取下一个号段填充备用Buffer;当前号段用尽后无缝切换,业务请求全程无阻塞。

  1. 解决数据库单点问题 → DB高可用架构

底层MySQL采用一主多从 + 故障自动切换,配合中间件实现主从转移,规避单库宕机风险;同时合理调大步长,DB宕机后,内存号段仍可支撑10~20分钟正常发号。

  1. 动态 step 自适应

号段更新时实时持久化max_id,服务重启后从数据库加载最新位置,服务运行过程中,会根据单个buffer消耗时长动态调整步长(比如:消耗时长小于15分钟,步长会加倍,消耗时长大于30分钟,步长会缩小原来1/2),在 DB 压力、ID 浪费中间动态平衡:

  • 短时间耗完号段→step 自动扩容,减少 DB 查询;

  • 长时间闲置号段→step 自动收缩,减少服务重启 ID 空洞;

3.1.4 优缺点 & 适用场景

  • 优点:严格单调递增、无时钟依赖、内存发号高性能、容灾强;

  • 缺点:ID 连续不安全、重启存在少量 ID 空洞、依赖 MySQL;

  • 适用:内部日志、流水、报表等非敏感业务;

  • 禁用:订单、用户 ID 等对外业务。


3.2 Leaf-snowflake 增强雪花模式

3.2.1 原生Snowflake结构复用

64位ID结构:1位符号位 + 41位毫秒时间戳 + 10位WorkerID + 12位序列号

  • 毫秒级时间戳:可用约69年

  • 10位WorkerID:支持最多1024个节点

  • 12位序列号:单毫秒内支持4096个ID

3.2.2 针对「原生Snowflake」核心缺点的优化

问题1:WorkerID 手动分配,运维复杂、易冲突

Leaf 解决方案:基于ZooKeeper自动分配WorkerID

  • 服务启动后连接ZK,创建持久顺序节点,利用ZK自增顺序号作为当前实例的WorkerID,全集群自动分配,无需人工配置,扩缩容零运维成本,彻底避免ID冲突

持久顺序节点:/snowflake/{leaf.name}/forever/{ip}:{port}-000000001

例如:/snowflake/leaf/forever/192.168.1.100:8080-000000001

  • 本地文件缓存WorkerID,ZK宕机重启可复用已有ID,降低强依赖;

本地文件:{java.io.tmpdir}/{leaf.name}/leafconf/{port}/workerID.properties

例如:/tmp/leaf/leafconf/8080/workerID.properties

文件内容:workerID=123

问题2:时钟回拨,导致ID重复(最致命问题)

Leaf 多层时钟回拨防护体系

  • 启动校验:启动时对比本机时间与ZK记录的历史最大时间,若发生回拨直接启动失败并告警;

  • 运行时实时监控:定时检测系统时间,小幅回拨(≤5ms)主动阻塞等待时间追平,不生成重复ID;

  • 大幅时间回拨:直接拒绝服务并触发报警,防止脏数据;

  • 运维兜底建议:生产环境关闭节点NTP自动时间同步,从源头减少时钟回拨概率;

3.2.3 优缺点 & 适用场景

  • 优点:ID 无序防爬虫、趋势递增、纯内存高性能、自动分配机器 ID 运维简单;

  • 缺点:新节点扩容依赖 ZK、无法彻底规避硬件时钟漂移;

  • 适用:订单、支付、用户 ID 等对外核心敏感业务。


四、全方案横向对比

实现方案 递增性 安全性(防泄露) 性能 可用性 核心缺陷
UUID 无序 中 高 极高 索引性能差
数据库自增 严格递增 差(连续) 低 低 DB单点、分库重复
多库步长自增 整体递增 低 中 中 扩容复杂、维护成本高
简易号段 严格递增 差(连续) 中高 中 阻塞、DB单点
Redis自增 严格递增 差(连续) 高 中 依赖Redis、ID连续
原生Snowflake 趋势递增 高 极高 中 时钟回拨、WorkerID难分配
Leaf-segment 严格递增 差(连续) 极高 极高 ID连续、少量空洞
Leaf-snowflake 趋势递增 高 极高 高 依赖ZK、时钟风险

选型原则

选 segment:内部业务、需要严格自增、不在意 ID 连续泄露;

选 snowflake:对外核心交易、防爬保密、只需趋势递增;

不推荐:绝对无空洞连续编号、无力做时钟运维的极简小项目。


posted @ 2026-07-10 20:06  七星6609  阅读(64)  评论(0)    收藏  举报