自增 ID 存在的问题及分布式 ID 解决方案(Redis 实战)
·
在秒杀、抢购等高并发业务场景中,系统往往需要快速生成订单并将其保存到订单表。
如果订单表直接使用 MySQL 自增 ID,在单体系统中尚可接受,但在高并发、分布式架构下,会暴露出一系列严重问题。
一、为什么不能直接使用 MySQL 自增 ID?
1 、ID 规律性太明显,存在安全隐患
MySQL 的自增 ID 是严格递增、连续的,例如:
10001 → 10002 → 10003
在实际业务中,这会带来多个风险:
- 容易被猜测:
攻击者可以通过已知 ID 推测出其他订单或用户数据 - 请求伪造风险:
直接构造请求访问不属于自己的订单 - 暴力遍历攻击:
根据 ID 规律批量扫描系统资源
ID 的可预测性过强,会直接降低系统的安全性。
2 、单表数据量受限,难以支撑长期增长
虽然 MySQL 理论上支持非常大的表,但在实际工程中,单表数据量是有上限的。
MySQL 行数限制(简要说明)
- MySQL 5.7 及之前:
- InnoDB 行数理论上最大约为 2³² − 1 ≈ 42 亿
- MySQL 8.0 及之后:
- 理论上可达到 2⁶⁴ − 1(几乎不可达)
⚠️ 但注意:
理论值 ≠ 工程可用值
在真实业务中,还会受到以下因素限制:
- 磁盘空间 索引大小 查询性能 备份、迁移成本
业界经验结论
单表数据量超过 500 万行,就需要考虑分库分表。
3、分库分表后,自增 ID 无法保证全局唯一
当订单表进行 水平拆分 后:
order_01:id = 1, 2, 3
order_02:id = 1, 2, 3
问题立刻出现:
- 不同库、不同表中的 ID 会重复 无法作为全局唯一主键 业务系统之间无法通过 ID 唯一定位数据
MySQL 自增 ID 天然不适合分布式架构
二、自增 ID 规律明显带来的综合问题
当 ID 具有明显规律时,会进一步引发以下问题:
1. 安全性问题
- 易被恶意用户分析、推测
- 容易遭受未授权访问和数据篡改
2. 隐私泄露风险
- 可通过 ID 推测出其他用户的订单或敏感数据
- 增加系统被“撞库”“爬数据”的风险
3. 数据可预测性过强
- 攻击者可推测系统当前订单量
- 不利于业务防刷、防爬虫
4. 扩展性受限
- 分库分表后无法继续使用自增策略
- 高并发下会形成写入热点
三、解决方案:分布式 ID(全局唯一 ID)
为了解决以上问题,引入 分布式 ID(Global Unique ID) 是业界通用方案。
分布式 ID 需要满足哪些特性?
- 全局唯一性 在整个分布式系统中不重复
- 高可用性 单点故障不影响 ID 生成
- 高性能 支撑高并发场景
- 安全性 ID 不可预测
- 趋势递增 有利于数据库索引和查询性能
四、常见分布式 ID 实现方案
| 方案 | 特点 |
|---|---|
| UUID | 简单,但过长、无序,索引性能差 |
| 数据库自增 | 不适合分布式 |
| Redis 自增 | 高性能,但需要设计 |
| Snowflake(雪花算法) | 经典方案 |
| 自定义组合方案 | 灵活、可控 |
五、Redis 分布式 ID 设计方案(时间戳 + 序列号)
在本项目中,采用 Redis + 时间戳 + 序列号 的方式实现分布式 ID。
1、ID 结构设计
| 符号位 | 时间戳 | 序列号 |
- 符号位(1 bit) 永远为 0,保证是正数
- 时间戳(31 bit) 秒级时间戳,可使用约 69 年
- 序列号(32 bit) 同一秒内的自增计数器 理论支持每秒生成 2³² 个 ID
最终生成的是一个 long 类型的全局唯一 ID
2、 Redis 分布式 ID 生成器实现
@Component
public class RedisIdWorker {
@Resource
private StringRedisTemplate stringRedisTemplate;
// 起始时间戳(2022-01-01 00:00:00)
private static final long BEGIN_TIMESTAMP = 1640995200;
// 序列号位数
private static final int COUNT_BITS = 32;
/**
* 生成分布式 ID
*/
public long nextId(String keyPrefix) {
// 1. 生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIMESTAMP;
// 2. 生成序列号(按天自增,防止无限增长)
String date = now.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
Long count = stringRedisTemplate.opsForValue()
.increment("icr:" + keyPrefix + ":" + date);
// 3. 拼接并返回
return timestamp << COUNT_BITS | count;
}
}
六、性能与并发测试
测试思路
- 使用 300 个线程
- 每个线程生成 100 个 ID
- 总计生成 3 万个 ID
- 验证: 是否重复 性能是否可接受
测试代码
@SpringBootTest
public class RedisIdWorkerTest {
@Resource
private RedisIdWorker redisIdWorker;
private ExecutorService es = Executors.newFixedThreadPool(500);
@Test
public void testNextId() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(300);
Runnable task = () -> {
for (int i = 0; i < 100; i++) {
long id = redisIdWorker.nextId("order");
System.out.println("id = " + id);
}
latch.countDown();
};
long begin = System.currentTimeMillis();
for (int i = 0; i < 300; i++) {
es.submit(task);
}
latch.await();
long end = System.currentTimeMillis();
System.out.println("生成 3 万个 ID 共耗时:" + (end - begin) + "ms");
}
}
七、总结
- MySQL 自增 ID 不适合高并发、分布式场景
- 存在: 安全风险 扩展性问题 分库分表冲突
- 分布式 ID 是秒杀、订单系统的必备组件
- 使用 Redis + 时间戳 + 序列号: 高性能 全局唯一 趋势递增 实现简单,可控性强
更多推荐




所有评论(0)