UUID 原理
UUID(Universally Unique Identifier)是一种广泛使用的唯一标识符标准。让我们深入了解 UUID 的原理和实现。
1. 定义
UUID 是一个 128 位的数字,用于在计算机系统中标识信息。其标准格式为 32 个十六进制数字,用连字符分为 5 组。
格式示例:
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
示例:
550e8400-e29b-41d4-a716-446655440000
结构分解:
550e8400-e29b-41d4-a716-446655440000
↑ ↑ ↑ ↑ ↑
时间戳 版本 变体 节点
8-4-4-4-12
2. UUID 标准
UUID 由 RFC 4122 标准定义,包含多个版本:
| 版本 | 描述 | 特点 |
|---|---|---|
| UUID v1 | 基于时间和节点 | 包含时间戳和 MAC 地址 |
| UUID v2 | 基于 DCE 安全 | 包含用户标识符 |
| UUID v3 | 基于 MD5 命名 | 基于命名空间和名称 |
| UUID v4 | 随机生成 | 完全随机 |
| UUID v5 | 基于 SHA-1 命名 | 基于 SHA-1 哈希 |
| UUID v7 | 基于时间排序 | 时间有序(新标准) |
1. 组成结构
时间低位 (32位)
时间中位 (16位)
时间高位和版本 (16位)
时钟序列和变体 (16位)
节点标识符 (48位)
示例分解:
550e8400-e29b-11d4-a716-446655440000
│ │ │ │ │
└── 60位时间戳
└─ 版本 (1)
└─ 13位时钟序列 + 变体
└─ 48位 MAC 地址
2. 生成过程
v1 的生成过程是「取时间 + 填版本 + 拼节点」三步:
1. 读当前时间,换算成从 1582-10-15 00:00:00 起算的 100 纳秒间隔数
2. 把这个 60 位时间戳拆成时间低位(32 位)、时间中位(16 位)、时间高位(16 位)
3. 把版本号 1 写进时间高位的低 4 位,把时钟序列和变体写进第 4 段
4. 节点标识符取本机网卡的 MAC 地址,取不到时随机生成并打上标记
5. 拼成 128 位,按标准格式输出成 36 个字符
时间戳的换算:
当前时间:2024-04-10 00:00:00
距 1582-10-15 的间隔:约 1.08 × 10¹⁷ 个 100 纳秒
60 位时间戳能表示的上限:约 1.15 × 10¹⁸
Java 实现:
// JDK 的 UUID.randomUUID() 生成的是 v4,v1 要用第三方库
// 依赖:com.fasterxml.uuid:java-uuid-generator
UUID v1 = Generators.timeBasedGenerator().generate();
时钟序列的作用是应对时钟回拨:时间戳回退时,序列号继续递增,保证这一毫秒内不会撞号。代价是 v1 会暴露 MAC 地址和时间信息,外部系统面前要谨慎使用。
3. 特点
优点:
- ✅ 时间有序(同一机器内)
- ✅ 生成速度快
- ✅ 无需协调
缺点:
- ❌ 暴露 MAC 地址(隐私问题)
- ❌ 时钟回拨可能导致重复
- ❌ 不同机器间无序
1. 组成结构
随机位 (122位)
版本标识 (4位)
变体标识 (2位)
示例分解:
f47ac10b-58cc-4372-a567-0e02b2c3d479
│ │ │ │ │
└── 48位随机
└─ 版本 (4)
└─ 变体
└─ 62位随机
2. 生成过程
v4 没有时间、没有节点,整段内容都是随机的,只在固定位置写入版本号和变体标识:
1. 生成 122 位随机数(SecureRandom)
2. 把版本号 4 写进第 6 字节的高 4 位
3. 把变体标识写进第 8 字节的高 2 位
4. 拼成 128 位,按标准格式输出成 36 个字符
随机数的来源:
JDK 用的是 SecureRandom,底层依赖操作系统的熵池(Linux 上是 /dev/urandom)。熵池只在实例启动时读一次,之后每次调用走的是内部状态推进,所以连续生成 UUID 并不会每次都去读系统。
Java 实现:
// v4:一次调用就够
UUID uuid = UUID.randomUUID();
性能: v4 的生成开销在微秒级,比数据库自增低一个数量级,比号段模式高一个数量级——它既不依赖网络,也不占共享资源,代价只是每次都要算一遍随机数。
3. 碰撞概率
计算公式:
碰撞概率 ≈ n² / (2 × 2¹²²)
其中 n 是生成的 UUID 数量
碰撞概率示例:
生成 10亿个 UUID:
碰撞概率 ≈ (10⁹)² / (2 × 2¹²²)
≈ 10¹⁸ / (2 × 5.3 × 10³⁶)
≈ 0
结论:
- 理论上有碰撞可能
- 实际上碰撞概率趋近于零
- 可以认为完全唯一
4. 特点
优点:
- ✅ 完全唯一
- ✅ 无需协调
- ✅ 生成速度快
- ✅ 分布式友好
缺点:
- ❌ 完全无序
- ❌ 较长(36字符)
- ❌ 存储空间大
1. 背景
UUID v7 是 2024 年新提出的标准,专门解决 UUID 无序的问题。
2. 组成结构
Unix 时间戳 (48位)
版本标识 (4位)
随机位 (74位)
变体标识 (2位)
示例分解:
0189dcd0-3e45-7123-8b5e-123456789abc
│ │ │ │ │
└── 48位时间戳
└─ 版本 (7)
└─ 随机位
└─ 变体
3. 生成过程
v7 是 2024 年推出的新标准,核心思路很直接:把「时间有序」写进 ID 的高位。
1. 取当前 Unix 毫秒时间戳,占 48 位
2. 写上版本号 7(4 位)
3. 剩下 74 位填随机数
4. 写上变体标识(2 位)
5. 拼成 128 位,按标准格式输出成 36 个字符
结构对照:
48 位时间戳 | 4 位版本 | 74 位随机 | 2 位变体
毫秒精度 固定 7 单调递增 固定 10
因为高 48 位就是时间戳,所以:
- 按字符串排序和按字节排序的结果一致,都是时间序
- 同一毫秒内的多个 ID 靠后面的随机位区分,不会重复
- 存进数据库时,ID 的插入顺序和生成顺序一致,聚簇索引不会频繁分裂
Java 实现:
// v7 需要第三方库
// 依赖:com.fasterxml.uuid:java-uuid-generator
UUID v7 = Generators.timeBasedEpochFirst().generate();
v7 的兼容性问题只有一个:老版本的标准库和数据库不认识版本号 7。少数老系统会把 v7 当成普通随机 UUID 处理,这没问题;真正要留意的是那些按固定格式解析 UUID 的旧程序。
4. 特点
优点:
- ✅ 时间有序
- ✅ 完全唯一
- ✅ 无需协调
- ✅ 存储空间小(19位)
缺点:
- ❌ 较新标准,兼容性可能有问题
Java
import java.util.UUID;
// v4:完全随机,最通用
UUID v4 = UUID.randomUUID();
System.out.println(v4); // 550e8400-e29b-41d4-a716-446655440000
// v7:时间有序,需要第三方库
// 依赖:com.fasterxml.uuid:java-uuid-generator
UUID v7 = Generators.timeBasedEpochFirst().generate();
// 解析:v7 的高 48 位是毫秒时间戳
long timestamp = v7.getMostSignificantBits() >>> 16;
Python
import uuid
# v4:完全随机
v4 = uuid.uuid4()
print(v4) # 550e8400-e29b-41d4-a716-446655440000
# v7:时间有序,需要 Python 3.14 及以上
v7 = uuid.uuid7()
# 解析:v7 的高 48 位是毫秒时间戳
timestamp = v7.int >> 80
Go
package main
import (
"fmt"
"github.com/google/uuid"
)
func main() {
// v4:完全随机
v4 := uuid.New()
fmt.Println(v4)
// v7:时间有序,需要 github.com/google/uuid v1.4.0 及以上
v7, err := uuid.NewV7()
if err != nil {
panic(err)
}
fmt.Println(v7)
// v1:基于时间和节点
v1, err := uuid.NewUUID()
if err != nil {
panic(err)
}
fmt.Println(v1)
}
适合使用 UUID 的场景
- 分布式系统:无需中央协调
- 消息 ID:消息追踪和去重
- 会话 ID:用户会话标识
- 日志追踪:请求追踪 ID
- 临时文件:临时文件命名
- 数据库主键:当有序性不重要时
不适合使用 UUID 的场景
- 订单号:需要可读和有序
- 用户 ID:需要短小和有序
- 支付流水:需要可追溯
- URL 短链:需要短小
了解了 UUID 的原理后,我们分析它的优缺点。
👉 下一节:优缺点分析
发现内容有问题?
告诉我们哪里需要改进。
