分库分表后的主键id冲突陷阱
当你的mysql数据库因数据量或并发压力进行分库分表后,主键id重复会成为系统崩溃的导火索。假设你将订单表拆分为10个分表,每个分表都从1开始自增,最终会出现以下灾难性问题:
- id冲突:不同分表中的订单id可能完全相同
- 业务混乱:无法通过id直接定位数据所在分表
- 分布式事务失败:主键冲突导致写入操作异常
本文将从底层原理出发,结合真实业务代码,深度解析8种主流主键id生成方案,助你构建稳定可靠的分库分表系统。
一、方案一:数据库自增id + 分段设置
核心思想
通过为每个分表配置不同的起始值和步长,确保id全局唯一。
实现代码
-- 分表1(t_order_0)配置 alter table t_order_0 auto_increment = 1; -- 起始值1,步长10 -- 分表2(t_order_1)配置 alter table t_order_1 auto_increment = 11; -- 起始值11,步长10 -- 分表3(t_order_2)配置 alter table t_order_2 auto_increment = 21; -- 起始值21,步长10
代码注解
- 步长设置:若总分表数为n,则每个分表步长设为n
- 起始值计算:分表索引i的起始值为
i * n + 1 - 适用场景:分表数量固定的小规模系统
java调用示例
// 根据订单id计算分表索引
int gettableindex(long orderid) {
return (int)(orderid % 10); // 假设分10个表
}
// 插入订单
void insertorder(order order) {
int tableindex = gettableindex(order.getid());
string tablename = "t_order_" + tableindex;
// 动态拼接sql
string sql = "insert into " + tablename + "(id, order_no) values (?, ?)";
jdbctemplate.update(sql, order.getid(), order.getorderno());
}
优缺点分析
| 优点 | 缺点 |
|---|---|
| 实现简单,无需额外组件 | 分表数量固定,扩容困难 |
| 性能优秀,直接利用数据库特性 | 高并发下单表自增id可能耗尽 |
| 存储效率高,id紧凑 | 需手动维护分表配置 |
二、方案二:uuid全局唯一标识符
核心思想
利用uuid的128位随机性保证全局唯一性。
实现代码
-- 创建订单表(主键字段为uuid)
create table t_order (
id char(36) primary key, -- uuid长度36位
order_no varchar(50)
);
java生成uuid
import java.util.uuid;
public class order {
private string id = uuid.randomuuid().tostring(); // 生成uuid
private string orderno;
// getter & setter
}
代码注解
- uuid格式:
xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx - 存储优化:可压缩为16字节二进制存储(base128编码)
性能优化示例
// 压缩uuid为16字节
public byte[] compressuuid(string uuid) {
return uuid.fromstring(uuid).tostring().getbytes(standardcharsets.utf_8);
}
优缺点分析
| 优点 | 缺点 |
|---|---|
| 完全随机,无依赖数据库 | 存储空间大(36字节) |
| 适用于分布式系统 | b+树索引性能差(随机写入) |
| 无需协调生成id | 无法直接定位分表 |
三、方案三:snowflake算法(twitter开源)
核心思想
通过时间戳+机器id+序列号生成64位有序id。
java实现代码
public class snowflakeidgenerator {
private final long twepoch = 1288834974657l; // 起始时间戳(2010-11-04)
private final long workeridbits = 5l; // 机器id位数
private final long datacenteridbits = 5l; // 数据中心id位数
private final long sequencebits = 12l; // 序列号位数
private final long maxworkerid = ~(-1l << workeridbits);
private final long maxdatacenterid = ~(-1l << datacenteridbits);
private final long sequencemask = ~(-1l << sequencebits);
private long workerid;
private long datacenterid;
private long sequence = 0l;
private long lasttimestamp = -1l;
public snowflakeidgenerator(long workerid, long datacenterid) {
if (workerid > maxworkerid || workerid < 0) {
throw new illegalargumentexception(string.format("worker id can't be greater than %d or less than 0", maxworkerid));
}
if (datacenterid > maxdatacenterid || datacenterid < 0) {
throw new illegalargumentexception(string.format("datacenter id can't be greater than %d or less than 0", maxdatacenterid));
}
this.workerid = workerid;
this.datacenterid = datacenterid;
}
public synchronized long nextid() {
long timestamp = timegen();
if (timestamp < lasttimestamp) {
throw new runtimeexception(string.format("时钟回拨: %d ms", lasttimestamp - timestamp));
}
if (lasttimestamp == timestamp) {
sequence = (sequence + 1) & sequencemask;
if (sequence == 0) {
timestamp = tilnextmillis(lasttimestamp);
}
} else {
sequence = 0;
}
lasttimestamp = timestamp;
return (timestamp - twepoch) << (workeridbits + datacenteridbits + sequencebits)
| (datacenterid << (workeridbits + sequencebits))
| (workerid << sequencebits)
| sequence;
}
protected long tilnextmillis(long lasttimestamp) {
long timestamp = timegen();
while (timestamp <= lasttimestamp) {
timestamp = timegen();
}
return timestamp;
}
protected long timegen() {
return system.currenttimemillis();
}
}
代码注解
- 位运算逻辑:
- 时间戳(41位) → 代表从起始时间到现在的毫秒数
- 数据中心id(5位) → 支持32个数据中心
- 机器id(5位) → 支持32个节点
- 序列号(12位) → 毫秒内最多支持4096个id
调用示例
snowflakeidgenerator idgenerator = new snowflakeidgenerator(1, 1);
long orderid = idgenerator.nextid();
system.out.println("生成的id: " + orderid);
优缺点分析
| 优点 | 缺点 |
|---|---|
| 全局唯一且有序 | 依赖系统时间(时钟回拨问题) |
| 支持分布式场景 | 需要预先分配机器id |
| 存储效率高(8字节) | 最大时间戳限制为69年 |
四、方案四:redis原子自增
核心思想
利用redis的incr命令生成全局唯一id。
redis配置
# 启动redis redis-server # 设置初始值 set order_id 1000
java调用示例
import redis.clients.jedis.jedis;
public class redisidgenerator {
private jedis jedis = new jedis("localhost");
public long generateid() {
return jedis.incr("order_id"); // 原子自增
}
}
代码注解
- 高可用保障:建议使用redis集群模式
- 分片策略:可通过不同key区分业务类型(如
order_id、user_id)
集群模式优化
// redis cluster客户端配置
jediscluster jediscluster = new jediscluster(new hostandport("192.168.1.101", 6379),
new hostandport("192.168.1.102", 6379));
优缺点分析
| 优点 | 缺点 |
|---|---|
| 高性能(每秒百万级qps) | 依赖redis服务稳定性 |
| 支持分布式场景 | 单点故障风险(需集群) |
| 简单易用 | 需处理网络延迟 |
五、方案五:第三方id生成服务
核心思想
使用独立服务(如滴滴tinyid)生成id。
tinyid集成示例
// 1. 添加maven依赖
<dependency>
<groupid>com.didi</groupid>
<artifactid>tinyid-client</artifactid>
<version>1.0.0</version>
</dependency>
// 2. 配置tinyid
tinyidclient client = new tinyidclient("http://tinyid-service.com");
// 3. 生成id
string businesstype = "order";
long id = client.getid(businesstype);
代码注解
- tinyid原理:基于数据库的sequence表生成id
- 多租户支持:通过
businesstype区分不同业务
tinyid服务端配置
create table tinyid (
id bigint primary key,
business_type varchar(50),
max_id bigint,
step int,
version int
);
优缺点分析
| 优点 | 缺点 |
|---|---|
| 支持多业务类型 | 需维护独立服务 |
| 高可用性(支持集群) | 依赖网络通信 |
| 灵活配置步长 | 学习成本较高 |
六、方案六:comb id(组合id)
核心思想
将时间戳与随机数组合,生成有序uuid。
java实现代码
import java.time.instant;
import java.util.random;
public class combidgenerator {
private final random random = new random();
public string generateid() {
byte[] uuid = new byte[16];
// 前6字节为时间戳
long timestamp = instant.now().toepochmilli();
for (int i = 0; i < 6; i++) {
uuid[i] = (byte)(timestamp >> (8 * (5 - i)));
}
// 后10字节为随机数
random.nextbytes(uuid);
return bytestohex(uuid);
}
private string bytestohex(byte[] bytes) {
stringbuilder sb = new stringbuilder();
for (byte b : bytes) {
sb.append(string.format("%02x", b));
}
return sb.tostring();
}
}
代码注解
- 时间戳部分:确保id有序性
- 随机部分:避免重复
优缺点分析
| 优点 | 缺点 |
|---|---|
| 兼具有序性和随机性 | 实现复杂度高 |
| 降低b+树索引碎片 | 依赖时间同步 |
七、方案七:数据库中间件内置策略
shardingsphere示例
// 1. 配置shardingsphere spring.shardingsphere.sharding.tables.t_order.key-generator.column=order_id spring.shardingsphere.sharding.tables.t_order.key-generator.type=snowflake spring.shardingsphere.sharding.tables.t_order.key-generator.props.worker.id=123
代码注解
- 内置算法:支持uuid和snowflake
- 自定义扩展:可通过接口实现自定义生成器
自定义生成器示例
public class customkeygenerator implements keygenerator {
@override
public comparable<?> generatekey() {
return new snowflakeidgenerator(1, 1).nextid();
}
}
优缺点分析
| 优点 | 缺点 |
|---|---|
| 无缝集成shardingsphere | 依赖中间件版本 |
| 支持多种算法 | 配置复杂度高 |
八、方案八:基因法 + hash分片
核心思想
在id中嵌入分片基因,直接定位分表。
java实现代码
public class shardidgenerator {
private static final int shard_count = 8; // 8个分表
private static final int gen_bits = 3; // 3位基因(2^3=8)
public long generateshardid(long baseid, int shardindex) {
// 基因掩码:0b00000111
long mask = (1 << gen_bits) - 1;
// 清除低3位基因
long idwithoutgene = baseid & ~mask;
// 设置新的基因
return idwithoutgene | (shardindex & mask);
}
}
代码注解
- 基因提取:通过位运算定位分表索引
- 适用场景:hash分片的分库分表系统
分表查询示例
long shardid = 1234567890123456789l; int shardindex = (int)(shardid & ((1 << 3) - 1)); // 提取低3位 string tablename = "t_order_" + shardindex;
优缺点分析
| 优点 | 缺点 |
|---|---|
| 直接定位分表 | id修改后需重新计算 |
| 无需额外组件 | 分片规则强依赖基因位 |
如何选择最适合你的方案?
| 方案 | 适用场景 | 推荐指数 |
|---|---|---|
| 数据库自增+步长 | 小规模分表 | ⭐⭐⭐ |
| uuid | 分布式系统 | ⭐⭐⭐⭐ |
| snowflake | 高并发系统 | ⭐⭐⭐⭐⭐ |
| redis | 高性能要求 | ⭐⭐⭐⭐ |
| tinyid | 多业务场景 | ⭐⭐⭐⭐ |
| comb id | 有序性优先 | ⭐⭐⭐ |
| shardingsphere | 中间件集成 | ⭐⭐⭐⭐ |
| 基因法 | hash分片 | ⭐⭐⭐⭐ |
最后提醒:选择主键生成方案时,需综合考虑业务规模、性能需求和运维成本。记住:没有银弹方案,只有最合适的解决方案!
代码包结构
mainkeygenerator/
├── config/
│ └── shard.properties # 分片配置
├── service/
│ ├── redisidservice.java # redis生成器
│ ├── snowflakeservice.java # snowflake实现
│ └── tinyidservice.java # tinyid客户端
├── model/
│ └── order.java # 订单模型
└── util/
└── shardutil.java # 分片工具类
部署建议:
- 生产环境建议采用snowflake或tinyid
- 高并发场景优先使用redis集群
- 小规模系统可使用数据库自增步长
“主键id是分库分表系统的命脉,选对方案,你的系统才能稳如泰山!”
以上就是mysql分库分表后主键id生成的八种方案的详细内容,更多关于mysql主键id生成方案的资料请关注代码网其它相关文章!
发表评论