文章目录

MySQL分库分表后全局ID生成策略Snowflake与Leaf对比

发布于 2026-06-21 15:48:57 · 浏览 4 次 · 评论 0 条

MySQL分库分表后全局ID生成策略Snowflake与Leaf对比

1. 为什么分库分表后需要全局ID

当单表数据量过大(如超过千万或亿级),或数据库读写压力达到瓶颈时,我们通常会采用分库分表策略。这会将数据分散到多个数据库的多张表中。此时,如果继续使用数据库的自增主键,将无法保证ID的全局唯一性,甚至在同一分片内都无法保证连续递增,这会给数据关联、迁移和查询带来巨大麻烦。

因此,我们需要一种能在分布式环境下生成全局唯一、趋势递增ID的方案。

2. Snowflake算法

Snowflake是Twitter开源的分布式ID生成算法,它不依赖数据库,完全在内存中生成ID,性能极高。

2.1 ID结构

一个Snowflake ID是一个64位的长整型数字,其二进制结构如下:

| 1 位无用位 | 41 位时间戳 | 10 位机器ID | 12 位序列号 |
  • 1 位无用位:固定为0,因为正整数的符号位是0。
  • 41 位时间戳:存储当前时间相对于一个固定起始时间(纪元,epoch)的毫秒差。可用约69年。
  • 10 位机器ID:唯一标识一台机器。通常分为5位数据中心ID和5位工作机器ID,理论上支持1024个节点。
  • 12 位序列号:在同一毫秒内,同一机器上生成的ID的顺序号,从0开始,每毫秒最多生成4096个ID。

可以用公式表示一个ID的组成:

$$ ID = (timestamp - epoch) << 22 | workerId << 12 | sequence $$

其中 << 是位移操作。

2.2 生成步骤

  1. 获取当前毫秒级时间戳 currentTimestamp
  2. 判断 currentTimestamp 与上次生成ID的时间戳 lastTimestamp
    • 如果相同(同一毫秒内),则 递增 序列号 sequence。当 sequence 达到 4095(二进制111111111111)后仍在此毫秒,需等待至下一毫秒,将 sequence 重置为 0
    • 如果 currentTimestamp 大于 lastTimestamp,说明进入了新的毫秒,将 sequence 重置为 0
    • 如果 currentTimestamp 小于 lastTimestamp,说明系统时钟回拨,抛出异常并拒绝生成ID。
  3. 计算 lastTimestamp = currentTimestamp
  4. 使用公式生成ID。

2.3 代码示例(关键逻辑)

public synchronized long nextId() {
    long currentTimestamp = System.currentTimeMillis();

    // 时钟回拨检查
    if (currentTimestamp < lastTimestamp) {
        throw new RuntimeException("Clock moved backwards.");
    }

    if (currentTimestamp == lastTimestamp) {
        // 相同毫秒内,序列号自增
        sequence = (sequence + 1) & SEQUENCE_MASK; // & 4095
        if (sequence == 0) {
            // 序列号溢出,等待下一毫秒
            currentTimestamp = waitNextMillis(lastTimestamp);
        }
    } else {
        // 不同毫秒,序列号重置
        sequence = 0L;
    }

    lastTimestamp = currentTimestamp;

    // 组装ID
    return ((currentTimestamp - EPOCH) << TIMESTAMP_LEFT_SHIFT) |
           (workerId << WORKER_ID_SHIFT) |
           sequence;
}

2.4 优缺点

  • 优点:纯内存操作,性能极高;ID趋势递增,对数据库索引友好;不依赖第三方服务,自身即中心。
  • 缺点:强依赖机器时钟,时钟回拨会导致重复ID或服务不可用;机器ID需要手动规划和分配,运维成本高;分布式环境下难以自适应扩展。

3. Leaf(号段模式)

Leaf是美团开源的分布式ID生成服务。它提供了两种模式:号段模式(Segment)和Snowflake模式。这里我们重点对比其更核心的号段模式

3.1 核心原理

号段模式的核心思想是从数据库批量获取ID。每次从数据库获取一个区间(称为“号段”),例如 (1000, 2000],然后在本地内存中依次分配这些ID。当号段使用到一定比例(如80%)时,会异步去数据库预取下一个号段,保证服务的高可用。

这个过程需要一张数据库表来记录当前业务分配到的最大ID:

CREATE TABLE `id_alloc` (
  `biz_tag` varchar(128) NOT NULL COMMENT ‘业务标识’,
  `max_id` bigint(20) NOT NULL COMMENT ‘当前分配到的最大ID’,
  `step` int(11) NOT NULL COMMENT ‘号段长度’,
  `description` varchar(256) DEFAULT NULL,
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB;

3.2 工作流程

  1. 服务启动时,或当前号段即将用完时,数据库发起一次请求,获取新的号段。
  2. 数据库执行一条更新语句(乐观锁),获取下一个可用号段。例如,step1000,当前 max_id2000,则本次分配的号段是 (2000, 3000],同时数据库中的 max_id 被更新为 3000
  3. 获取到的号段 (2000, 3000] 加载到内存中。
  4. 业务请求,内存中的当前号段 分配 一个ID。例如第一个请求分配 2001,第二个 2002...
  5. 监控当前号段已使用比例。当达到阈值(如80%,即已分配到 2800)时,触发异步线程,去预取下一个号段((3000, 4000]),实现平滑过渡。

3.3 双Buffer优化

为了彻底消除数据库访问对服务的影响,Leaf采用了双Buffer机制

  • Buffer 1:当前正在提供服务的号段。
  • Buffer 2:已经预加载好的下一个号段。

当Buffer 1使用到一定比例时,切换到Buffer 2提供服务,同时后台线程填充 Buffer 1,使其变为下一个可用号段。如此循环,保证始终有一个Buffer处于可用状态,即使数据库暂时不可用,服务也能持续一段时间。

3.4 优缺点

  • 优点:不依赖时钟,无时钟回拨问题;ID趋势递增;容灾能力强,双Buffer机制允许数据库短暂故障时服务不中断;业务标识隔离,易于管理。
  • 缺点:ID不再是连续的,只是趋势递增,因为每次获取的是一个范围;依赖外部数据库(但仅用于取号段,访问频率极低)。

4. Snowflake 与 Leaf(号段模式)对比

对比维度 Snowflake Leaf(号段模式)
依赖 无外部依赖,自身闭环 依赖数据库(用于获取号段)
性能 极高(纯内存位运算) 高(内存分配,仅号段用尽时访问DB)
ID 特性 整体趋势递增,同一毫秒内无序 严格趋势递增
唯一性保障 依赖机器ID分配和时钟不回拨 依赖数据库事务和业务隔离
时钟依赖 强依赖,时钟回拨会导致问题 无依赖
容灾能力 机器ID管理复杂,节点故障需人工介入 双Buffer机制支持数据库短暂故障,容灾性更好
运维成本 需要手动规划与分配10位WorkerID 只需维护一张数据库表,更简单
适用场景 对ID格式无严格要求,追求极致性能,节点规模可控的场景 需要更稳定可靠、易于扩展和运维的绝大多数业务场景

5. 如何选择

  1. 选择 Snowflake 的场景

    • 对ID生成性能有极致要求(每秒百万级以上)。
    • 环境可控,能妥善解决机器ID分配问题,并能通过NTP等手段保证服务器时钟的精准与同步。
    • 不希望引入任何外部组件(如数据库)。
  2. 选择 Leaf 号段模式的场景

    • 追求高可用稳定,需要较强的容灾能力。
    • 希望ID服务易于运维,无需操心复杂的节点标识和时钟问题。
    • 业务量大但ID生成速度并非极致瓶颈(通常能满足绝大多数业务需求)。
    • 需要为不同业务线隔离ID空间。

对于大多数采用MySQL分库分表的互联网应用,Leaf的号段模式因其高可用、易运维的特性,通常是更稳妥和主流的选择。Snowflake则更适用于对性能有极致追求,且具备较强运维能力来管理分布式节点的场景。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文