分库分表下的 ID 冲突问题与雪花算法讲解
大家好,我是工藤学编程 🦉 | 一个正在努力学习的小博主,期待你的关注 |
---|---|
实战代码系列最新文章😉 | C++实现图书管理系统(Qt C++ GUI界面版) |
SpringBoot实战系列🐷 | 【SpringBoot实战系列】Sharding-Jdbc实现分库分表到分布式ID生成器Snowflake自定义wrokId实战 |
环境搭建大集合 | 环境搭建大集合(持续更新) |
分库分表 | 分库分表技术栈讲解-Sharding-JDBC |
前情摘要:
1、数据库性能优化
2、分库分表之优缺点分析
3、分库分表之数据库分片分类
4、分库分表之策略
5、分库分表技术栈讲解-Sharding-JDBC
本文章目录
- (一) 分库分表下的ID冲突问题与分布式ID生成方案
- 分布式ID生成方案对比
- 1. 数据库自增ID(改进版)
- 2. UUID(通用唯一识别码)
- 3. Redis发号器
- 4. Snowflake雪花算法
- 数据库号段模式(Leaf-Segment)
- 美团Leaf方案
- 方案选型建议
- (二)雪花算法(Snowflake)详解
- 一、雪花算法的本质与起源
- 二、64位ID的结构拆解
- 三、Java实现示例(含时钟回拨处理)
- 四、雪花算法的核心坑点与解决方案
- 坑一:分布式环境下workId重复
- 坑二:系统时钟回拨导致ID重复
- 五、雪花算法的适用场景
- 六、与其他ID方案的对比
- 总结
前言:在进行实操之前,我们还需要走最后一步,那就是了解分库分表下的 ID 冲突问题
(一) 分库分表下的ID冲突问题与分布式ID生成方案
传统自增ID的局限性:
- 单库环境:MySQL通过
AUTO_INCREMENT
自动生成唯一主键 - 分库分表后:不同分片的自增ID会重复(如库1的订单表ID为1,库2的订单表ID也可能为1)
冲突示例:
-- 分库前:单库自增ID保证唯一性
INSERT INTO orders(id, user_id) VALUES(NULL, 1001); -- 自动生成ID=1-- 分库后:库1和库2各自生成ID=1
库1: INSERT INTO orders(id, user_id) VALUES(NULL, 1001); -- ID=1
库2: INSERT INTO orders(id, user_id) VALUES(NULL, 1002); -- ID=1(冲突)
分布式ID生成方案对比
1. 数据库自增ID(改进版)
原理:通过设置不同的自增步长和初始值,使各库生成不重复ID。
配置示例:
-- 库1:从1开始,步长2(生成1,3,5...)
SET @@auto_increment_offset = 1;
SET @@auto_increment_increment = 2;-- 库2:从2开始,步长2(生成2,4,6...)
SET @@auto_increment_offset = 2;
SET @@auto_increment_increment = 2;
优缺点:
✅ 实现简单,依赖数据库原生功能
❌ 扩容困难(新增分片需重新规划步长)
❌ 主从切换可能导致ID重复
❌ 性能瓶颈(单库生成ID)
2. UUID(通用唯一识别码)
原理:基于随机数或时间戳生成全局唯一字符串(如550e8400-e29b-41d4-a716-446655440000
)。
Java实现:
String uuid = UUID.randomUUID().toString();
优缺点:
✅ 无网络开销,性能高
✅ 完全去中心化,生成逻辑简单
❌ 无序字符串,不适合作为索引(影响查询性能)
❌ 存储空间占用大(36字节)
❌ 不具备趋势自增特性(不利于数据库分区分页)
3. Redis发号器
原理:利用Redis的原子操作INCR
和INCRBY
生成唯一ID。
示例代码:
// 获取下一个订单ID
Long orderId = redisTemplate.opsForValue().increment("order_id_generator", 1);
优缺点:
✅ 高性能(Redis单线程原子操作)
✅ 支持批量生成(减少网络调用)
❌ 依赖外部服务(Redis故障影响ID生成)
❌ 需维护Redis集群,增加系统复杂度
4. Snowflake雪花算法
原理:生成64位长整型ID,结构如下:
1位符号位 | 41位时间戳 | 5位数据中心ID | 5位机器ID | 12位序列号
- 时间戳:精确到毫秒级,保证生成的ID按时间趋势递增
- 机器ID:确保不同服务器生成不同ID
- 序列号:同一毫秒内生成的不同ID
优缺点:
✅ 高性能(本地生成,无网络开销)
✅ 趋势自增(有利于数据库索引优化)
✅ 可自定义位分配(适应不同业务场景)
❌ 依赖系统时钟(时钟回拨可能导致ID重复)
❌ 机器ID需提前规划(分布式环境下需唯一分配)
数据库号段模式(Leaf-Segment)
原理:从数据库批量获取ID号段,本地内存分配,减少数据库访问。
示例:
- 数据库表存储当前号段的最大值(如
max_id=1000
) - 应用获取号段(如
1-1000
),本地自增生成ID - 用完后再从数据库获取下一号段(如
1001-2000
)
优缺点:
✅ 高性能(本地生成,仅号段用完时访问数据库)
✅ 不依赖时钟
❌ 存在ID空洞(号段未用完时应用重启)
❌ 需数据库表支持
美团Leaf方案
特点:结合Snowflake和号段模式,提供两种ID生成方式:
- Leaf-Segment:数据库号段模式,适合对时钟敏感的业务
- Leaf-Snowflake:雪花算法,通过ZooKeeper分配机器ID
方案选型建议
方案 | 性能 | 唯一性 | 趋势自增 | 依赖外部服务 | 时钟敏感性 | 适用场景 |
---|---|---|---|---|---|---|
数据库自增ID | 低 | ✅ | ✅ | ✅(数据库) | ❌ | 小规模分库(<4个节点) |
UUID | 高 | ✅ | ❌ | ❌ | ❌ | 对ID格式无要求的场景 |
Redis发号器 | 中高 | ✅ | ✅ | ✅(Redis) | ❌ | 已有Redis集群的场景 |
Snowflake | 高 | ✅ | ✅ | ❌ | ✅ | 高性能、分布式场景 |
数据库号段模式 | 中高 | ✅ | ✅ | ✅(数据库) | ❌ | 对时钟回拨敏感的业务 |
(二)雪花算法(Snowflake)详解
一、雪花算法的本质与起源
定义:由Twitter开源的分布式ID生成算法,通过64位长整型数字(long
类型)生成全局唯一、趋势递增的ID。
核心优势:
- 高性能(本地生成,无网络开销)
- 趋势递增(适合数据库索引优化)
- 结构可解析(通过ID反推生成时间、机器等信息)
二、64位ID的结构拆解
1位符号位 | 41位时间戳 | 5位数据中心ID | 5位机器ID | 12位序列号
- 1位符号位:固定为0(保证生成正数)。
- 41位时间戳:
- 精确到毫秒,可使用69年((2^41-1)/1000/60/60/24/365 ≈ 69年)。
- 通常设置为“起始时间戳”(如2015-01-01)与当前时间的差值,避免负数。
- 5位数据中心ID:最多支持32个数据中心(2^5=32)。
- 5位机器ID:每个数据中心最多支持32台机器(2^5=32)。
- 12位序列号:同一毫秒内最多生成4096个ID(2^12=4096)。
三、Java实现示例(含时钟回拨处理)
public class SnowflakeIdGenerator {// 起始时间戳(2021-01-01 00:00:00)private final long startTimestamp = 1609459200000L;// 各部分位数private final long dataCenterIdBits = 5L; // 数据中心ID位数private final long workerIdBits = 5L; // 机器ID位数private final long sequenceBits = 12L; // 序列号位数// 最大取值计算private final long maxWorkerId = -1L ^ (-1L << workerIdBits); // 31private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits); // 31private final long maxSequence = -1L ^ (-1L << sequenceBits); // 4095// 位移偏移量private final long workerIdShift = sequenceBits;private final long dataCenterIdShift = sequenceBits + workerIdBits;private final long timestampShift = sequenceBits + workerIdBits + dataCenterIdBits;// 实例变量private long dataCenterId; // 数据中心IDprivate long workerId; // 机器IDprivate long sequence = 0L; // 序列号private long lastTimestamp = -1L; // 上次生成ID的时间戳// 构造函数(参数需提前规划分配)public SnowflakeIdGenerator(long dataCenterId, long workerId) {// 参数校验if (dataCenterId > maxDataCenterId || dataCenterId < 0) {throw new IllegalArgumentException(String.format("DataCenter ID can't be greater than %d or less than 0", maxDataCenterId));}if (workerId > maxWorkerId || workerId < 0) {throw new IllegalArgumentException(String.format("Worker ID can't be greater than %d or less than 0", maxWorkerId));}this.dataCenterId = dataCenterId;this.workerId = workerId;}// 同步生成ID(避免并发冲突)public synchronized long nextId() {long timestamp = currentTimeMillis();// 时钟回拨处理(核心坑点)if (timestamp < lastTimestamp) {long offset = lastTimestamp - timestamp;if (offset <= 5) {// 短时间回拨:等待至lastTimestamp后再生成try {wait(offset);timestamp = currentTimeMillis();if (timestamp < lastTimestamp) {throw new RuntimeException("Clock moved backwards. Refusing to generate id.");}} catch (InterruptedException e) {throw new RuntimeException(e);}} else {// 长时间回拨:直接抛异常(需人工处理)throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", offset));}}// 同一毫秒内if (timestamp == lastTimestamp) {// 序列号自增,达到上限则等待下一毫秒sequence = (sequence + 1) & maxSequence;if (sequence == 0) {timestamp = waitNextMillis(lastTimestamp);}} else {// 新毫秒,序列号重置为0sequence = 0L;}lastTimestamp = timestamp;// 组合各部分生成IDreturn ((timestamp - startTimestamp) << timestampShift) |(dataCenterId << dataCenterIdShift) |(workerId << workerIdShift) |sequence;}// 获取当前时间戳private long currentTimeMillis() {return System.currentTimeMillis();}// 等待至下一毫秒private long waitNextMillis(long lastTimestamp) {long timestamp = currentTimeMillis();while (timestamp <= lastTimestamp) {timestamp = currentTimeMillis();}return timestamp;}
}
四、雪花算法的核心坑点与解决方案
坑一:分布式环境下workId重复
问题:不同机器分配相同workId,导致生成ID重复。
解决方案:
- 人工分配:小规模集群手动规划(如数据中心ID=0,机器ID按机房机柜编号分配)。
- 自动分配:
- 通过ZooKeeper抢占节点(如在
/snowflake/worker_ids
下创建临时顺序节点,节点序号作为workId)。 - 基于数据库表记录已分配的workId,获取时自增+1。
- 通过ZooKeeper抢占节点(如在
坑二:系统时钟回拨导致ID重复
问题场景:
- 手动修改服务器时间(如NTP时钟同步)。
- 虚拟机暂停后恢复(CPU时间片调度导致时间回拨)。
解决方案:
- 轻度回拨(<5ms):
- 等待回拨时间后再生成ID(如代码示例中的
wait(offset)
)。
- 等待回拨时间后再生成ID(如代码示例中的
- 重度回拨(>5ms):
- 抛异常阻断业务(适合强一致性场景)。
- 切换至备用ID生成方案(如UUID),并记录告警。
- 预防措施:
- 禁止生产环境手动修改系统时间。
- 服务器开启NTP自动同步(避免大幅时间偏差)。
五、雪花算法的适用场景
适用场景:
- 核心业务ID生成:订单号、用户ID、交易ID等(需趋势递增,便于数据库排序)。
- 高并发系统:如秒杀、抢购场景(高性能+无网络依赖)。
- 分布式微服务:跨节点ID唯一性要求高的场景。
不适用场景:
- 对ID安全性要求高的场景(ID结构可解析,可能泄露业务量等信息)。
- 对时钟敏感的场景(如金融交易,时钟回拨可能引发严重问题)。
六、与其他ID方案的对比
方案 | 雪花算法 | UUID | Redis发号器 | 数据库号段 |
---|---|---|---|---|
唯一性 | ✅ | ✅ | ✅ | ✅ |
趋势递增 | ✅ | ❌ | ✅ | ✅ |
性能 | 高(本地计算) | 高(无网络) | 中(依赖网络) | 中(批量获取) |
依赖 | 系统时钟 | 无 | Redis集群 | 数据库 |
时钟敏感 | ✅(回拨需处理) | ❌ | ❌ | ❌ |
总结
雪花算法通过“时间戳+机器标识+序列号”的结构,在分布式场景下实现了高性能、唯一且有序的ID生成。其核心挑战在于时钟回拨处理和机器ID分配,生产环境中需结合业务特点制定针对性方案。对于追求高性能和ID有序性的场景,雪花算法是首选;若对时钟敏感或ID安全性要求高,则需考虑其他方案(如数据库号段或UUID)。