分库分表后的主键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生成方案的资料请关注代码网其它相关文章!
发表评论