什么是 UUID,能保证唯一性吗?
结论:UUID(Universally Unique Identifier)是 128 位的标识符,标准写法是 32 个十六进制字符加 4 个连字符,共 36 个字符。它不能"保证"唯一,只能在概率意义上"实际唯一":v4 有 122 位随机数,生成 10 亿个的碰撞概率约 10⁻¹⁹;但概率不为零,而且如果随机源熵不足、节点状态被克隆(虚拟机快照、容器镜像复制),重复仍会发生。所以 UUID 适合做请求 ID、消息 ID、业务主键,不适合做需要严格单调或强唯一的场景。
1. 128 位怎么排布
标准字符串:xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
└─ 8 ─┘ └4┘ └4┘ └4┘ └── 12 ──┘ 共 36 字符
字段:time_low(32) | time_mid(16) | time_hi_and_version(16)
| clock_seq_and_variant(16) | node(48)
其中第 13 个十六进制位是 version(1~8),第 17 个十六进制位是 variant
(IETF 标准里高 2 位为 10,所以该位通常是 8/9/a/b)
2. 常见版本
| 版本 | 生成依据 | 随机性 | 是否有序 | 典型用途 |
|---|---|---|---|---|
| v1 | 60 位时间戳 + 时钟序列 + MAC 地址 | 低 | 大致有序 | 遗留系统;会暴露机器 MAC |
| v3 | 命名空间 + 名字的 MD5 | 确定性 | 否 | 由名字稳定推导 ID |
| v4 | 122 位随机数 | 高 | 否 | 通用 ID、请求追踪 |
| v5 | 命名空间 + 名字的 SHA-1 | 确定性 | 否 | 同 v3,安全性更好 |
| v7 | 48 位 Unix 毫秒 + 74 位随机 | 高 | 是 | 数据库主键、日志 ID(RFC 9562) |
v3/v5 是"确定性"的:同样的命名空间和名字永远得到同一个 UUID,适合做"内容寻址";v4 是"随机"的,适合做"每次都要新"的标识。
3. 唯一性到底有多强
生日悖论(birthday problem)给出近似:生成 n 个 122 位随机值,碰撞概率约为
P ≈ n² / (2 × 2¹²²)
n = 10⁶ → P ≈ 10¹² / (2 × 5.3 × 10³⁶) ≈ 10⁻²⁵
n = 10⁹ → P ≈ 10¹⁸ / (2 × 5.3 × 10³⁶) ≈ 10⁻¹⁹
n = 10¹² → P ≈ 10²⁴ / (2 × 5.3 × 10³⁶) ≈ 10⁻¹⁴
碰撞概率开始变得不可忽略,大约在 n 接近 √(2 × 2¹²²) ≈ 2.3 × 10¹⁸ 时;按每秒 10 亿个算,也要连续生成七十多年才到这个量级。"概率极低"不等于"绝对保证",这就是标准里用 "unique" 而非 "guaranteed unique" 的原因,也是为什么 UUID 需要配合数据库唯一索引做兜底。
4. 代码:生成与存储
import java.nio.ByteBuffer;
import java.util.UUID;
public class UuidDemo {
public static void main(String[] args) {
UUID id = UUID.randomUUID(); // 基于 SecureRandom,v4
System.out.println(id); // 例如 3f1c...-...-4...-a...-...
System.out.println(id.version()); // 4
System.out.println(id.variant()); // 2,表示 IETF 变体
System.out.println(id.toString().replace("-", "").length()); // 32
// 存 BINARY(16) 而不是 CHAR(36):省空间,也利于索引
byte[] bytes = new byte[16];
ByteBuffer bb = ByteBuffer.wrap(bytes);
bb.putLong(id.getMostSignificantBits());
bb.putLong(id.getLeastSignificantBits());
// 从 16 字节还原
ByteBuffer back = ByteBuffer.wrap(bytes);
UUID restored = new UUID(back.getLong(), back.getLong());
System.out.println(restored.equals(id)); // true
}
}
UUID.randomUUID() 内部用 SecureRandom,首次初始化可能较慢;QPS 极高的场景可以预热,但不要换成 Random——它的种子空间与可预测性都会让"唯一"变成"撞库"。
5. 当主键合适吗
| 方案 | 存储长度 | 有序 | 索引友好度 | 分布式唯一 |
|---|---|---|---|---|
自增 BIGINT | 8 字节 | 是 | 高 | 否(依赖中心) |
UUID v4 CHAR(36) | 36 字节 | 否 | 低,易页分裂 | 是 |
UUID v4 BINARY(16) | 16 字节 | 否 | 中 | 是 |
UUID v7 BINARY(16) | 16 字节 | 是 | 高 | 是 |
雪花 ID BIGINT | 8 字节 | 是 | 高 | 是(依赖时钟与 workerId) |
InnoDB 的主键是聚簇索引(clustered index),无序主键会让插入位置随机,导致页分裂、缓存命中率下降;二级索引还要存一份主键,36 字节的字符串会让索引显著膨胀。要用 UUID 做主键,至少用 BINARY(16),最好用 v7 这类时间有序的版本。
6. 常见追问
| 追问 | 回答要点 |
|---|---|
| 能截断 UUID 吗? | 不建议,每截掉 4 位,熵就少 16 位,碰撞概率指数上升 |
| v1 有什么风险? | 含 MAC 地址与时间戳,可反推机器与生成时刻,泄露隐私 |
| 为什么虚拟机里会重复? | 快照/镜像复制会让多个实例共享同一随机源状态;容器场景要确保熵源独立 |
| UUID 能做幂等键吗? | 可以,客户端生成后重试复用同一个 UUID,服务端按它去重 |
| 和雪花算法怎么选? | 要 8 字节、有序、可读时间 → 雪花;要跨语言、无需协调、可接受 16 字节 → UUID v7 |
| UUID v7 在 Java 里怎么生成? | JDK 暂未内置,可用第三方库或按 RFC 9562 自行拼装(毫秒时间戳 + 随机位) |
小结
UUID 是"概率唯一"的 128 位标识,v4 靠随机、v1 靠时间与 MAC、v7 靠时间有序。回答时把握三点:36 字符格式、122 位随机带来的碰撞概率、以及无序主键对 InnoDB 索引的伤害。