大家好,我是工藤学编程 🦉 | 一个正在努力学习的小博主,期待你的关注 |
|---|---|
实战代码系列最新文章😉 | C++实现图书管理系统(Qt C++ GUI界面版) |
SpringBoot实战系列🐷 | 【SpringBoot实战系列】Sharding-Jdbc实现分库分表到分布式ID生成器Snowflake自定义wrokId实战 |
环境搭建大集合 | 环境搭建大集合(持续更新) |
分库分表 | 分库分表技术栈讲解-Sharding-JDBC |
前情摘要:
1、数据库性能优化 2、分库分表之优缺点分析 3、分库分表之数据库分片分类 4、分库分表之策略 5、分库分表技术栈讲解-Sharding-JDBC
前言:在进行实操之前,我们还需要走最后一步,那就是了解分库分表下的 ID 冲突问题
传统自增ID的局限性:
AUTO_INCREMENT自动生成唯一主键冲突示例:
-- 分库前:单库自增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:从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)
原理:基于随机数或时间戳生成全局唯一字符串(如550e8400-e29b-41d4-a716-446655440000)。
Java实现:
String uuid = UUID.randomUUID().toString();优缺点: ✅ 无网络开销,性能高 ✅ 完全去中心化,生成逻辑简单 ❌ 无序字符串,不适合作为索引(影响查询性能) ❌ 存储空间占用大(36字节) ❌ 不具备趋势自增特性(不利于数据库分区分页)
原理:利用Redis的原子操作INCR和INCRBY生成唯一ID。
示例代码:
// 获取下一个订单ID
Long orderId = redisTemplate.opsForValue().increment("order_id_generator", 1);优缺点: ✅ 高性能(Redis单线程原子操作) ✅ 支持批量生成(减少网络调用) ❌ 依赖外部服务(Redis故障影响ID生成) ❌ 需维护Redis集群,增加系统复杂度
原理:生成64位长整型ID,结构如下:
1位符号位 | 41位时间戳 | 5位数据中心ID | 5位机器ID | 12位序列号 优缺点: ✅ 高性能(本地生成,无网络开销) ✅ 趋势自增(有利于数据库索引优化) ✅ 可自定义位分配(适应不同业务场景) ❌ 依赖系统时钟(时钟回拨可能导致ID重复) ❌ 机器ID需提前规划(分布式环境下需唯一分配)
原理:从数据库批量获取ID号段,本地内存分配,减少数据库访问。 示例:
max_id=1000)1-1000),本地自增生成ID1001-2000)优缺点: ✅ 高性能(本地生成,仅号段用完时访问数据库) ✅ 不依赖时钟 ❌ 存在ID空洞(号段未用完时应用重启) ❌ 需数据库表支持
特点:结合Snowflake和号段模式,提供两种ID生成方式:
方案 | 性能 | 唯一性 | 趋势自增 | 依赖外部服务 | 时钟敏感性 | 适用场景 |
|---|---|---|---|---|---|---|
数据库自增ID | 低 | ✅ | ✅ | ✅(数据库) | ❌ | 小规模分库(<4个节点) |
UUID | 高 | ✅ | ❌ | ❌ | ❌ | 对ID格式无要求的场景 |
Redis发号器 | 中高 | ✅ | ✅ | ✅(Redis) | ❌ | 已有Redis集群的场景 |
Snowflake | 高 | ✅ | ✅ | ❌ | ✅ | 高性能、分布式场景 |
数据库号段模式 | 中高 | ✅ | ✅ | ✅(数据库) | ❌ | 对时钟回拨敏感的业务 |
定义:由Twitter开源的分布式ID生成算法,通过64位长整型数字(long类型)生成全局唯一、趋势递增的ID。
核心优势:
1位符号位 | 41位时间戳 | 5位数据中心ID | 5位机器ID | 12位序列号 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); // 31
private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits); // 31
private 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; // 数据中心ID
private long workerId; // 机器ID
private 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 {
// 新毫秒,序列号重置为0
sequence = 0L;
}
lastTimestamp = timestamp;
// 组合各部分生成ID
return ((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,导致生成ID重复。 解决方案:
/snowflake/worker_ids下创建临时顺序节点,节点序号作为workId)。问题场景:
wait(offset))。适用场景:
不适用场景:
方案 | 雪花算法 | UUID | Redis发号器 | 数据库号段 |
|---|---|---|---|---|
唯一性 | ✅ | ✅ | ✅ | ✅ |
趋势递增 | ✅ | ❌ | ✅ | ✅ |
性能 | 高(本地计算) | 高(无网络) | 中(依赖网络) | 中(批量获取) |
依赖 | 系统时钟 | 无 | Redis集群 | 数据库 |
时钟敏感 | ✅(回拨需处理) | ❌ | ❌ | ❌ |
雪花算法通过“时间戳+机器标识+序列号”的结构,在分布式场景下实现了高性能、唯一且有序的ID生成。其核心挑战在于时钟回拨处理和机器ID分配,生产环境中需结合业务特点制定针对性方案。对于追求高性能和ID有序性的场景,雪花算法是首选;若对时钟敏感或ID安全性要求高,则需考虑其他方案(如数据库号段或UUID)。