UUID 原理

UUID(Universally Unique Identifier)是一种广泛使用的唯一标识符标准。让我们深入了解 UUID 的原理和实现。

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基于时间排序时间有序(新标准)

UUID v1:基于时间和节点

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 地址(隐私问题)
  • ❌ 时钟回拨可能导致重复
  • ❌ 不同机器间无序

UUID v4:随机生成

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字符)
  • ❌ 存储空间大

UUID v7:基于时间排序

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 的应用场景

适合使用 UUID 的场景

  1. 分布式系统:无需中央协调
  2. 消息 ID:消息追踪和去重
  3. 会话 ID:用户会话标识
  4. 日志追踪:请求追踪 ID
  5. 临时文件:临时文件命名
  6. 数据库主键:当有序性不重要时

不适合使用 UUID 的场景

  1. 订单号:需要可读和有序
  2. 用户 ID:需要短小和有序
  3. 支付流水:需要可追溯
  4. URL 短链:需要短小

下一步

了解了 UUID 的原理后,我们分析它的优缺点。

👉 下一节:优缺点分析

发现内容有问题?

告诉我们哪里需要改进。