MySQL分库分表主键重复解决方案:Snowflake雪花算法详解
1. 引言
随着业务数据量的快速增长,单库 MySQL 往往难以承受高并发和大数据存储压力。分库分表成为常见的数据库水平扩展方案:
- 分库:将数据分散到多个数据库实例
- 分表:将同一个数据库的数据分散到多张物理表
但是分库分表带来了一个新的问题:
如何保证全局主键唯一性?
在单表中我们可以直接用 AUTO_INCREMENT
自增 ID 作为主键,但在分库分表场景下:
- 每个表自增 ID 独立,容易产生重复
- 分布式系统需要全局唯一的主键标识
解决方案之一就是使用 Snowflake 雪花算法 生成全局唯一 ID。
2. 分库分表的主键重复问题
假设我们将用户表 user
分成 4 张表:
user_0, user_1, user_2, user_3
每张表用 MySQL 自增主键:
CREATE TABLE user_0 (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100)
);
如果每张表的自增 ID 都从 1 开始:
user_0.id: 1,2,3...
user_1.id: 1,2,3...
user_2.id: 1,2,3...
问题:全局范围内会出现大量重复 ID,无法唯一标识一条记录。
3. 分布式全局唯一 ID 生成方案
在分布式系统中,常见的全局唯一 ID 生成方案包括:
UUID
- 优点:简单,不依赖数据库
- 缺点:长度长(128bit),无序,索引性能差
数据库号段(Hi/Lo)
- 优点:自增,有序
- 缺点:依赖数据库,扩展性一般
雪花算法(Snowflake) ✅
- 优点:高性能、本地生成、趋势递增、有序可读
- 缺点:需要时钟正确性保证
4. Snowflake 雪花算法原理
Snowflake 是 Twitter 开源的分布式唯一 ID 生成算法,生成 64 位整型 ID(long
)。
4.1 ID 结构
| 1bit 符号位 | 41bit 时间戳 | 10bit 机器ID | 12bit 自增序列 |
详细结构:
符号位 (1bit)
- 永远为 0(保证正数)
时间戳 (41bit)
- 单位毫秒
- 可使用约 69 年(2^41 / (1000606024365))
机器ID (10bit)
- 可支持 1024 个节点
- 一般拆为
5bit数据中心ID + 5bit机器ID
序列号 (12bit)
- 每毫秒最多生成 4096 个 ID
4.2 ID 组成图解
0 | 41bit timestamp | 5bit datacenter | 5bit worker | 12bit sequence
例如:
0 00000000000000000000000000000000000000000
00001 00001 000000000001
5. Java 实现 Snowflake 算法
public class SnowflakeIdGenerator {
private final long workerId; // 机器ID
private final long datacenterId; // 数据中心ID
private long sequence = 0L; // 毫秒内序列
// 起始时间戳
private final long twepoch = 1609459200000L; // 2021-01-01
private final long workerIdBits = 5L;
private final long datacenterIdBits = 5L;
private final long sequenceBits = 12L;
private final long maxWorkerId = ~(-1L << workerIdBits); // 31
private final long maxDatacenterId = ~(-1L << datacenterIdBits);// 31
private final long sequenceMask = ~(-1L << sequenceBits); // 4095
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException("workerId out of range");
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException("datacenterId out of range");
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
// 时钟回拨处理
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards!");
}
if (lastTimestamp == timestamp) {
// 同毫秒内递增
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 毫秒内序列用尽,等待下一毫秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - twepoch) << (5 + 5 + 12))
| (datacenterId << (5 + 12))
| (workerId << 12)
| sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
6. MySQL 分库分表应用方案
6.1 业务架构图
+-----------------------+
| 应用服务 (Java) |
+-----------------------+
|
v
+-----------------------------+
| Snowflake ID 生成器 (本地) |
+-----------------------------+
|
v
+-------------------------+
| Sharding JDBC / MyCat |
+-------------------------+
| | |
v v v
DB0.User DB1.User DB2.User
流程:
- 应用启动本地 Snowflake 生成器(分配 datacenterId 和 workerId)
- 插入数据时生成全局唯一 ID
- Sharding-JDBC 根据分片键路由到指定库表
- 全局主键不冲突
6.2 插入数据示例
long userId = snowflake.nextId();
jdbcTemplate.update("INSERT INTO user (id, name) VALUES (?, ?)", userId, "Alice");
6.3 优势
- 本地生成,无中心化瓶颈
- 趋势递增,索引性能好
- 支持高并发:单机可达 \~400 万 ID/s
7. 实战优化与注意事项
时钟回拨问题
- Snowflake 依赖时间戳,如果系统时间回拨,可能导致重复 ID
- 解决:使用 NTP 同步时间,或加逻辑等待
机器 ID 分配
- 可用 ZooKeeper / Etcd 分配 workerId
- 或使用配置文件固定
高并发优化
- 使用无锁 LongAdder 或分段锁提高吞吐
- 结合 RingBuffer 做异步批量生成(如 Leaf Segment 模式)
8. 总结
在 MySQL 分库分表场景下:
- 使用 MySQL 自增 ID 会产生主键冲突
- UUID 太长且无序
- Snowflake 雪花算法是最优解之一
评论已关闭