事故后的第一反应
退款事故复盘会上,同事小刘第一个举手:换 UUID 啊!本地生成、不用协调、概率上全球不重复,一行代码的事。老王先点头——这确实是分布式 ID 的第一候选——然后打开订单表的空间监控:如果真换了,订单主键索引要涨三倍多。UUID 是好东西,但好东西也要看放哪。这篇把它的功与过拆清楚。
先认清这把刀
UUID 是 128 位的标识符,标准写法 36 个字符,按 8-4-4-4-12 分段:550e8400-e29b-41d4-a716-446655440000。它是个家族,不是一种东西:
| 版本 | 怎么生成 | 特点 |
|---|---|---|
| v1 | 时间戳加 MAC 地址 | 有序但泄露机器 MAC,时序字段在高位尾部 |
| v3 / v5 | 命名空间加名字做哈希 | 同名同号,可用于幂等去重 |
| v4 | 纯随机 122 位 | 最常用,完全无序 |
| v7 | 毫秒时间戳打头加随机尾部 | 趋势有序,2024 年进 RFC 9562 的新宠 |
Java 里 UUID.randomUUID() 用的就是 v4。它的核心优势一句话:发号不依赖任何人——没有 DB、没有 Redis、没有协调服务,本机生成,吞吐只受 CPU 限制。122 位随机空间,一纳秒生成一个也要上百亿年才有可观碰撞概率,唯一性靠概率而非协调,这是它和后面所有方案的本质区别。
当 InnoDB 主键的三重暴击
优势讲完,泼三盆冷水。第一盆:无序写,页分裂狂魔。InnoDB 是聚簇索引,数据按主键物理有序存放。自增 ID 永远往最后一页追加;v4 UUID 随机落点,新行可能插到任何一页——页满了就分裂、挪数据、维护父节点,写放大直接拉满。写入吞吐掉一个量级不是夸张,是 MySQL 系列里页分裂机制(第 2 篇)的必然结果。
第二盆:太肥,索引全面膨胀。bigint 主键 8 字节,UUID 存成 char(36) 是 36 字节起步。更要命的是 InnoDB 的二级索引叶子节点都存着一份主键值——订单表十几个二级索引,每个都要跟着胖。老王算过账:一亿行的订单表,主键从 bigint 换成 UUID,光索引空间多出几个 GB,缓冲池里能缓存的页少了,命中率跟着掉。索引是一寸金一寸土的地方。
第三盆:语义为零。随机串没有时间信息、没法排序、没法范围查询,按时间拉订单还得回表查别的列。运营说「把昨晚八点后的订单拉出来」,自增 ID 和雪花 ID 都能走主键范围,UUID 只能全表筛。
v7 与 ULID:改良,但没根治
社区早就看穿了无序的痛点,给出两条改良路线:UUID v7 把毫秒时间戳放在最高位,整体趋势有序,插入落点集中在 B+ 树右侧,页分裂大幅缓解;ULID 同思路,128 位里时间戳占 48 位,剩下的随机,还做了大小写友好的 Crockford 编码。两者都值得收录进工具箱。
但注意改良的边界:时间戳前缀牺牲了一点随机性(同一毫秒内的序靠随机尾部维持),并且长度问题纹丝没动——128 位还是 128 位,索引还是那么肥。所以 v7 适合替代 v4 的场合,不适合替代 bigint 主键的场合。
场景判断:什么时候用,什么时候别用
| 场景 | 用不用 | 理由 |
|---|---|---|
| 链路追踪 traceId | 用 | 全链路透传,不落库索引,无序无所谓 |
| 幂等键、去重键 | 用 | v3/v5 同名同号的特性正好 |
| 离线数据同步冲突避免 | 用 | 两端各自生成不打架 |
| 订单表主键 | 别用 | 无序写页分裂加索引膨胀 |
| 需要按时间范围检索的表 | 别用 v4 | 语义为零,范围查询全靠回表 |
小结
UUID 一句话:解决唯一性的成本最低,但把成本转嫁给了索引和写入。它是最顺手的瑞士军刀——traceId、幂等键、离线同步样样精通;当订单主键却是三重暴击。数据库主键需要的是趋势递增,这就引出下一步:继续让数据库发号,但换一种聪明的方式——号段模式。
评论 (0)