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 生成步骤
- 获取当前毫秒级时间戳
currentTimestamp。 - 判断
currentTimestamp与上次生成ID的时间戳lastTimestamp。- 如果相同(同一毫秒内),则 递增 序列号
sequence。当sequence达到4095(二进制111111111111)后仍在此毫秒,需等待至下一毫秒,将sequence重置为0。 - 如果
currentTimestamp大于lastTimestamp,说明进入了新的毫秒,将sequence重置为0。 - 如果
currentTimestamp小于lastTimestamp,说明系统时钟回拨,抛出异常并拒绝生成ID。
- 如果相同(同一毫秒内),则 递增 序列号
- 计算
lastTimestamp = currentTimestamp。 - 使用公式生成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 工作流程
- 服务启动时,或当前号段即将用完时,向数据库发起一次请求,获取新的号段。
- 数据库执行一条更新语句(乐观锁),获取下一个可用号段。例如,
step为1000,当前max_id为2000,则本次分配的号段是(2000, 3000],同时数据库中的max_id被更新为3000。 - 将获取到的号段
(2000, 3000]加载到内存中。 - 对业务请求,从内存中的当前号段 分配 一个ID。例如第一个请求分配
2001,第二个2002... - 监控当前号段已使用比例。当达到阈值(如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. 如何选择
-
选择 Snowflake 的场景:
- 对ID生成性能有极致要求(每秒百万级以上)。
- 环境可控,能妥善解决机器ID分配问题,并能通过NTP等手段保证服务器时钟的精准与同步。
- 不希望引入任何外部组件(如数据库)。
-
选择 Leaf 号段模式的场景:
- 追求高可用和稳定,需要较强的容灾能力。
- 希望ID服务易于运维,无需操心复杂的节点标识和时钟问题。
- 业务量大但ID生成速度并非极致瓶颈(通常能满足绝大多数业务需求)。
- 需要为不同业务线隔离ID空间。
对于大多数采用MySQL分库分表的互联网应用,Leaf的号段模式因其高可用、易运维的特性,通常是更稳妥和主流的选择。Snowflake则更适用于对性能有极致追求,且具备较强运维能力来管理分布式节点的场景。

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