美团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  阅读(32)  评论(0)    收藏  举报