📚 2.3.1 全局唯一 ID
一、 核心痛点:为什么不能用数据库自增 ID?
在以前简单的单体项目中,我们习惯把数据库主键设置为 AUTO_INCREMENT(自增)。但在高并发的电商或秒杀场景中,这种做法有三大致命缺陷:
- id 规律太明显(安全性低): 自增 ID 是 1, 2, 3 连着的。如果你的订单 ID 是自增的,竞争对手只要今天下一单,明天下一单,两个 ID 一减,就能精确推算出你一天的真实订单量!
- 受单表数据量限制(无法分库分表): 当订单表数据达到千万级别,必须拆分成多个表(甚至多个库)。如果继续用自增,表 A 和表 B 都会生成 ID 为 1 的订单,这就会导致主键冲突。
- 数据库性能瓶颈: 秒杀瞬间有极高并发的写请求,完全依赖 MySQL 来生成 ID,MySQL 的抗压能力会成为整个系统的短板。
二、 全局唯一 ID 的四大黄金标准
为了应对分布式高并发场景,我们需要自己设计或引入外部组件来生成 ID。一个合格的全局唯一 ID 必须满足以下条件:
- 唯一性: 绝对不能出现重复的 ID(最基本的要求)。
- 高可用与高性能: 生成 ID 的速度必须极快,且不能因为 ID 生成器宕机导致整个下单流程阻塞。
- 递增性: ID 最好是整体趋势递增的。因为 MySQL InnoDB 引擎的索引是 B+ 树结构,递增的主键能极大提高数据库的插入性能,减少页分裂。
- 安全性: ID 不能带有明显的连号规律,防止被恶意推算。
三、 主流 ID 生成策略大比拼
在业界,解决全局唯一 ID 通常有以下几种方案:
| 方案 | 原理与特点 | 优点 | 缺点 |
|---|---|---|---|
| UUID | 基于机器网卡、时间等生成的 32 位字符串。 | 本地生成,性能极高,绝对唯一。 | 无序的字符串,作为 MySQL 主键会导致索引性能极差;且太长,占用空间。 |
| 雪花算法 (Snowflake) | Twitter 开源的算法,利用 64 位 Long 型划分出时间戳、机器号、序列号。 | 性能极高,不依赖外部中间件,趋势递增。 | 强依赖机器时钟,如果时钟回拨会导致 ID 重复或生成失败。 |
| Redis 方案 | (本项目采用) 利用 Redis 的 INCR 命令实现自增,拼接时间戳。 | 不依赖数据库,Redis 内存操作性能极高,天然单线程保证原子性。 | 需要引入 Redis 中间件,增加了一次网络开销。 |
四、 本项目的 Redis 唯一 ID 设计图解
为了兼顾性能、递增性和安全性,本项目巧妙地利用了 Long 类型(64 个 bit 位)和 Redis 的自增特性,设计了如下的 ID 结构:
64 个 bit 位的具体划分:
- 符号位(1 bit): 永远为
0,保证 ID 都是正数。 - 时间戳(31 bit): 以秒为单位。记录的是当前时间减去某个自定义的起始时间(比如 2022-01-01 00:00:00)。31 个 bit 最大可以表示 秒,大约能用 69 年。
- 序列号(32 bit): 真正的自增数字,秒内的并发号。支持每秒产生 个不同的 ID(高达 42 亿),这对于任何秒杀系统都绝对够用了!
💡 Redis Key 的绝妙设计(按天自增):
在调用 Redis 的 INCR 命令时,我们不会用一个固定不变的 Key(比如 icr:order),而是会在 Key 后面拼接上当天的日期(比如 icr:order:20260308)。
- 好处 1: 防止 32 位的序列号被无限累加最终溢出。每天从 0 重新开始算,绝对安全。
- 好处 2(白嫖的统计功能): 因为 Key 是按天划分的,日后只要查一下 Redis 里某个日期的 Key 的值,就能直接知道这一天总共生成了多少个订单,连数仓统计都省了!
五、 学习总结
这一节的核心在于转变思维:从单机时代的“依赖数据库分配 ID”,转向分布式时代的“由应用程序或缓存中间件主动计算分配 ID”。 使用 Redis 拼接“时间戳 + 序列号”的方案,既保证了高性能,又隐藏了真实的单量信息。