什么是 UUID,能保证唯一性吗?

#Java#面试八股

什么是 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. 常见版本

版本生成依据随机性是否有序典型用途
v160 位时间戳 + 时钟序列 + MAC 地址低大致有序遗留系统;会暴露机器 MAC
v3命名空间 + 名字的 MD5确定性否由名字稳定推导 ID
v4122 位随机数高否通用 ID、请求追踪
v5命名空间 + 名字的 SHA-1确定性否同 v3,安全性更好
v748 位 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. 当主键合适吗

方案存储长度有序索引友好度分布式唯一
自增 BIGINT8 字节是高否(依赖中心)
UUID v4 CHAR(36)36 字节否低,易页分裂是
UUID v4 BINARY(16)16 字节否中是
UUID v7 BINARY(16)16 字节是高是
雪花 ID BIGINT8 字节是高是(依赖时钟与 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 索引的伤害。

笔记加载中…