在秒杀、抢购等高并发业务场景中,系统往往需要快速生成订单并将其保存到订单表。
如果订单表直接使用 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 需要满足哪些特性?

  1. 全局唯一性   在整个分布式系统中不重复
  2. 高可用性     单点故障不影响 ID 生成
  3. 高性能        支撑高并发场景
  4. 安全性          ID 不可预测
  5. 趋势递增       有利于数据库索引和查询性能

四、常见分布式 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 + 时间戳 + 序列号:  高性能   全局唯一   趋势递增   实现简单,可控性强
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐