UUID.randomUUID() 是 Java 开发中极为常用的一个工具方法,从数据库主键到分布式追踪 ID,它的身影无处不在。长久以来,开发者们已经习惯了它“能用就行”的定位。
然而,UUID 第 4 版(即我们常用的版本)存在一个众所周知的痛点:它完全基于随机数生成,没有包含任何时间信息。当这种随机值被用作数据库的 B+ Tree 索引键时,会导致大量的页分裂和随机写操作。在高写入场景下,这无疑是一个严重的性能瓶颈。
好消息是,随着 Java 26 的发布,这个问题终于有了官方的解决方案——原生支持时间有序的 UUIDv7。

UUIDv4 的主要缺陷
1. 索引性能差
UUIDv4 本质上是一个 122 位的随机数。新插入的记录,其主键值会随机分布在已有的数据页中,这导致数据库存储引擎(尤其是像 MySQL InnoDB 这类使用聚簇索引的引擎)必须频繁地进行随机 IO 来定位插入位置。与单调递增的自增 ID 相比,这种“随机写”模式会显著拖慢写入速度。
实测表明,在千万级数据量的表中,使用 UUIDv4 作为主键,其写入性能可能比使用自增 ID 慢 3-5 倍,同时索引体积也会增大 30-50%。
2. 无法按时间排序
由于缺乏时间戳信息,你无法直接通过 UUIDv4 的值来判断记录的创建先后顺序。许多团队不得不在表中额外增加一个 created_at 字段来弥补这一缺陷。
3. 信息密度低
128 位的空间全部用于承载随机性,除了保证全局唯一外,不携带任何其他(如时间、版本)信息,从信息利用的角度看,效率不高。
UUIDv7:时间有序的新标准
2024年5月,RFC 9562 正式发布,其中定义了 UUIDv7 规范。它的核心改进在于数据结构:
UUIDv7 结构(128 bits):
┌─────────────────────────────────────┐
│ 48-bit Unix Timestamp (毫秒精度) │ ← 前 48 位是时间戳
│ 4-bit Version (0111) │
│ 12-bit Random │
│ 2-bit Variant (10) │
│ 62-bit Random │ ← 剩余 74 位随机数
└─────────────────────────────────────┘
关键优势:
- 时间有序:高位是毫秒级 Unix 时间戳,生成的 ID 能大致按时间顺序排列。
- 索引友好:新记录的主键值会自然地追加在索引尾部,将随机写转变为近似顺序写,极大提升了 数据库(如 MySQL)的写入性能。
- 保持唯一性:剩余的 74 位随机数保证了在同一毫秒内生成重复 ID 的概率极低。
Java 26 终于原生支持了
此前,如果想在 Java 项目中使用 UUIDv7,你需要引入第三方库(如 com.fasterxml.uuid:java-uuid-generator)。从 Java 26 开始,这成为了标准库的一部分。
// Java 26 新增 API
UUID uuid = UUID.nameUUIDv7();
// 提取时间戳
Instant timestamp = uuid.getTimestamp();
// 一行代码搞定,无需额外依赖
重要提示:UUID.randomUUID() 方法仍将继续生成 UUIDv4。生成 v7 版本需要使用新增的 UUID.nameUUIDv7() 方法,请注意区分。
注意:有序但非严格递增
UUIDv7 是“时间有序”的,但这并不等同于“严格递增”。在同一毫秒内生成的多个 UUID,其顺序由随机数部分决定,因此可能出现局部乱序。
对于绝大多数场景(如数据库主键、通用的分布式 ID),这种“毫秒级有序 + 毫秒内随机”的模式已经完全够用。只有在要求绝对严格递增的场景(例如作为消息队列的精确位移),才需要在应用层实现带计数器的逻辑。
// 实现单调递增 UUIDv7 的伪代码示例
public class MonotonicUUIDv7 {
private long lastTimestamp = 0;
private long counter = 0;
public synchronized UUID next() {
long now = System.currentTimeMillis();
if (now == lastTimestamp) {
counter++;
} else {
lastTimestamp = now;
counter = 0;
}
// 用 counter 替换随机数的高位部分
return buildUUIDv7(now, counter);
}
}
UUIDv7 vs Snowflake vs ULID
在选择分布式 ID 方案时,我们通常会有多个候选。下表对比了三种常见方案:
| 对比维度 |
UUIDv7 |
Snowflake |
ULID |
| 长度 |
128 bit (36 字符) |
64 bit (19 位数字) |
128 bit (26 字符) |
| 时间精度 |
毫秒 |
毫秒 |
毫秒 |
| 有序性 |
毫秒级有序 |
严格递增 |
毫秒级有序 |
| 标准化 |
RFC 9562 |
无(各家实现不同) |
社区规范 |
| 外部依赖 |
无 |
需要 Worker ID 分配机制 |
需要第三方库 |
| JDK 原生 |
Java 26+ |
❌ |
❌ |
| 适合场景 |
分布式系统通用 ID |
高并发严格递增场景 |
需要紧凑、URL友好编码的场景 |
选型建议:
- 默认选择 UUIDv7:标准化程度高,即将成为 JDK 原生支持,性能和通用性平衡得很好。
- 需要严格递增且空间紧凑:可以考虑 Snowflake,但需自行解决 Worker ID 的分配问题。
- 需要更短的、URL友好的字符串:ULID 的 Crockford Base32 编码(26字符)是更好的选择。
迁移指南:从 v4 到 v7
对于新项目:毫不犹豫地使用 UUID.nameUUIDv7(),没有理由再选择 v4。
对于已有项目:可以逐步迁移,无需一次性改造所有存量数据。
// 1. 新记录开始使用 v7
UUID newId = UUID.nameUUIDv7();
// 2. 兼容性处理:判断已有 UUID 的版本
UUID existingId = UUID.fromString("...");
if (existingId.version() == 4) {
// 旧数据,v4 格式
} else if (existingId.version() == 7) {
// 新数据,v7 格式
}
UUIDv4 和 v7 可以在同一张表中共存,它们都是标准的 UUID 格式,仅版本号不同。迁移可以平滑进行:新数据使用 v7,老数据保持 v4 不变。
数据库层面:如果主键字段类型是 CHAR(36) 或 BINARY(16),则无需修改表结构。索引在接入 v7 数据后,其效率会随着新数据的顺序插入而自动得到改善。
技术的进步正是为了解决这些日积月累的痛点。Java 26 对 UUIDv7 的原生支持,为 后端 开发者们提供了一个更优雅、更高效的分布式 ID 生成选择。如果你对这类技术演进和实践感兴趣,欢迎在 云栈社区 与我们交流讨论。