以前在做业务建表时,为了图省事不依赖中心化自增发号器,大家习惯随手在主键上填一个 UUID v4(即 crypto.randomUUID())。在几十万条数据的小表里,系统跑得飞快,没有任何异样。

但当数据规模冲过几百万甚至上千万时,噩梦就来了:压测中数据库 CPU 突然飙到 100%,写入 QPS 从几千直接掉到两三百,磁盘 I/O 读写疯狂打满。查了 InnoDB Buffer Pool 命中率和 page_splits 统计指标才幡然醒悟——完全随机的 UUID v4 正在不断把 B+ 树叶子节点撕开,引发灾难性的页分裂和随机磁盘 Read/Write

2024 年 5 月,IETF 发布的 RFC 9562 正式推出了 UUID v7。这篇文章从底层 B+ 树物理存储机制讲起,聊聊为什么现代数据库主键应该全面拥抱 UUID v7,以及我们在纯本地 UUID 生成器里做出的设计权衡。


1. 经典 UUID 的回顾与硬伤

UUID(Universally Unique Identifier)是一个 128 位(16 字节)的标识符,标准呈现格式为 36 个字符的连字符十六进制字符串: xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx(其中 MM 为版本号,NN 为变体 variant)。

UUID v1:基于时间戳与 MAC 地址

  • 原理:使用 60 位的时间戳(自 1582 年以来的 100 纳秒间隔)+ 时钟序列 + 48 位的网卡 MAC 地址。
  • 致命缺陷隐私泄露。任何拿到 UUID v1 的人都能轻易逆向出生成该 ID 的物理机网卡 MAC 地址;此外在分布式集群中很难保证时钟严格单调递增。

UUID v4:纯随机生成(目前使用最广泛)

  • 原理:128 位中除保留 4 位版本号(0100 即 4)和 2 位变体号(10)外,其余 122 位全部由密码学安全随机数(CSPRNG)生成
  • 优点:简单、无状态、客户端随意生成、绝对保护隐私、碰撞概率在数学上可忽略不计(约为 1/21221/2^{122})。
  • 致命痛点完全无序(Non-sequential)。这在数据库场景下是致命的毒药。

2. 为什么 UUID v4 会搞崩数据库索引?

主流关系型数据库(如 MySQL InnoDB、SQL Server、PostgreSQL)的表存储结构通常采用 B+ 树聚簇索引(Clustered Index):数据物理行直接存储在主键索引树的叶子节点上。

自增 ID(Auto-Increment)的优势:顺序追加

当使用自增整型主键时,新插入的记录永远按递增顺序追加在 B+ 树的最后一个数据页中。写满一页就分配下一页,几乎没有页碎片,磁盘顺序写入性能极高。

UUID v4 的灾难:B+ 树页分裂(Page Splits)与缓存失效

因为 UUID v4 是随机散列的,每一条新记录都可能落在索引树的任意一个中间叶子节点

  1. 页分裂(Page Split):当新记录要插入的叶子页已经存满时,数据库引擎必须分配一个新数据页,并将原页面中约 50% 的数据移动过去。这伴随着大量的内存搬迁、日志写入和锁竞争。
  2. 随机磁盘 I/O:当数据量超出数据库内存缓冲池(Buffer Pool)时,插入一条新记录可能需要将冷数据页从磁盘加载到内存,修改后写回磁盘。此时数据库写吞吐量会发生断崖式暴跌(甚至下降 80%~95%)
  3. 索引膨胀:频繁的页分裂导致大量数据页未填满,索引体积可能比顺序主键大出 30%~50%,极大地浪费内存缓存。

3. 救赎者:UUID v7 的设计原理

为了彻底解决 UUID v4 的索引问题,同时保留分布式、无中心化协调的优点,RFC 9562 正式确立了 UUID v7

UUID v7 的 128 位二进制布局

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           unix_ts_ms                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          unix_ts_ms           |  ver  |       rand_a          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var|                        rand_b                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            rand_b                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • 高 48 位(前 6 字节):精确到毫秒级的 Unix 时间戳(unix_ts_ms)。
  • 4 位版本标识:固定为 0111(即十六进制 7)。
  • 12 位随机数 / 序列号(rand_a:支持在同一毫秒内的微纳秒精度或计数序列。
  • 2 位变体标识:固定为 10(RFC Variant)。
  • 低 62 位随机数(rand_b:密码学随机数,保证高并发下的全局唯一性。

核心特性

  1. 时间单调可排序(Time-Ordered):由于高位是标准 Unix 时间戳,按字节或字符串字典序排序与按时间生成顺序完全一致。插入数据库时,新记录天然追加在 B+ 树尾部,性能媲美自增整数,页分裂归零
  2. 免除额外的时间字段:直接从 UUID v7 的前 48 位就可以解析出精确到毫秒的创建时间戳,甚至可以在很多日志和事件表中省略 created_at 字段。
  3. 完全兼容现有 UUID 字段:仍然是标准的 128 位二进制 / 36 字符字符串,现有的数据库 UUID 类型或 CHAR(36) 列无需改动架构即可直接平滑迁移。

4. UUID 家族主流方案选型速查

方案 排序性 分布式无中心 数据库索引友好度 典型应用场景
自增 ID (BIGINT) 严格有序 需中心发号器 最好 单体架构、内部非敏感业务
Snowflake (雪花算法) 时间粗略有序 依赖 Worker ID 极好 大厂分布式中间件(有运维成本)
UUID v4 完全无序 极佳(纯随机) 极差 会话 Token、临时文件命名、无索引场景
UUID v7 时间严格单调 极佳(无依赖) 极好 现代微服务、新项目数据库主键首选

5. 纯浏览器端本地 UUID 生成与工具

在日常测试、API 调试、数据迁移或生成 Mock 数据时,经常需要批量生成唯一的 UUID。

你可以直接使用我们网站提供的免费纯本地工具:

  • UUID 生成器:基于浏览器原生的 crypto.getRandomValues() 密码学随机安全源,支持单次批量生成 1 到 100 个 UUID,一键极速复制整列;
  • JSON 格式化工具:本地秒级格式化、排版与校验含大量 UUID 记录的复杂 JSON 数据;
  • Base64 编码解码器:对二进制 UUID 或字符串做编解码。

所有工具均 100% 在你的浏览器本地完成,没有任何数据会发送给外部服务器,安全、私密且支持离线使用。