UUID(通用唯一识别码)是一种用于唯一标识信息的标准. 它是一个 128 位的值,通常表示为 32 个十六进制数字,以连字符分隔成 5组,格式为 8-4-4-4-12,例如: 550e8400-e29b-41d4-a716-446655440000.
UUID 的标准由开放软件基金会(OSF)定义,并被记录在 RFC 4122 中, 2024 年提出了一个新的 UUID 提案 RFC 9562,替代 RFC 4122.
首先了解一下 UUID, 然后在了解新的提案中的新的 UUID 版本,尤其重点 UUID v7 版本.
UUID 具有以下特点:
-
唯一性: 理论上, UUID 的重复概率极低, 可以认为是唯一的.
-
分布式系统友好: 不需要中央协调即可生成, 适用于分布式系统.
-
跨平台: 大多数编程语言和数据库系统都支持 UUID.
所以 UUID 在许多应用中都有广泛的使用:
-
数据库:用作主键,避免冲突.
-
分布式系统:用于唯一标识不同节点或服务.
-
文件名或资源标识:确保唯一性,避免命名冲突.
-
会话标识:在网络通信中唯一标识用户会话.
UUID 通常表示为一个 128 位的数字,通常以 32 个十六进制数字表示,并包含 4 个连字符. 标准的 UUID 表示格式如下:
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
-
xxxxxxxx: 前 8 位 -
xxxx: 后 4 位 -
M: UUID 的版本 -
N: UUID 的变体 -
xxxxxxxxxxxx: 最后 12 位
下表为 UUIDv4 版本/变体布局示例,其中“M”代表十六进制的版本位置0x4(0b0100)的表示,“N”代表变体放置四个可能的十六进制表示之一变体 10xx:0x8 (0b1000)、0x9 (0b1001)、0xA (0b1010)、0xB (0b1011).
00000000-0000-4000-8000-000000000000
00000000-0000-4000-9000-000000000000
00000000-0000-4000-A000-000000000000
00000000-0000-4000-B000-000000000000
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
版本M字段在 UUID 的第 13 个字符位置(48bit ~ 51bit),变体N字段在 UUID 的第 17 个字符位置(64bit ~ 68bit).
RFC 4122 中定义了 UUID 的 5 个版本,RFC 9562 中又增加了 6,7,8 三个版本. UUID 版本 1~5 缺乏某些期望的特征,例如:
-
不按时间顺序排列的 UUID 版本,例如 UUIDv4,数据库索引局部性较差,不适合做数据库中的主键.
-
UUIDv1 时间戳中使用的 100 纳秒,并不常见.
-
需要进行内省/解析才能按时间顺序排序,因为不是能够执行简单的逐字节比较.
-
会出现隐私和网络安全问题,如 UUIDv1 的节点字段中使用 MAC 地址等.
-
RFC4122 中规定的许多实施细节涉及权衡.
-
RFC4122 没有区分生成 UUID 和仅存储 UUID 的方式的不同.
所以新的规范又增加了 v6 ~ v8 三个版本,以解决上述问题. 新规范分析了下面 16 种 ID 的生成方法,可以说是煞费苦心:
-
ULID
-
LexicalUUID
-
Snowflake
-
Flake
-
ShardingID
-
KSUID
-
Elasticflake
-
FlakeID
-
Sonyflake
-
orderedUuid
-
COMBGUID
-
SID
-
pushID
-
XID
-
ObjectID
-
CUID
UUID 不同的版本的特点、生成方式、优缺点如下面的表格所示.
-
t: 时间相关位
-
n: 节点相关位(如 MAC 地址)
-
r: 随机或伪随机位
-
h: 哈希值位
-
s: 序列号位
-
v: 版本号位
-
x: 自定义位
| 版本 | 特点 | 生成方式 | 优点 | 缺点 | ASCII 格式 |
|---|---|---|---|---|---|
| UUID v1 | 基于时间和节点 | 使用当前时间戳、时钟序列和 MAC 地址 | 可排序;保证唯一性 | 可能泄露 MAC 地址;时间戳可预测 | tttttttt-tttt-1ttt-snnn-nnnnnnnnnnnn |
| UUID v2 | DCE 安全版本 | 类似 v1,但替换时间戳的前 4 位为 POSIX UID 或 GID | 适用于特定安全环境 | 使用较少;可能泄露系统信息 | tttttttt-tttt-2ttt-snnn-nnnnnnnnnnnn |
| UUID v3 | 基于名字的 MD5 版本 | 使用 MD5 哈希算法和命名空间 | 相同输入产生相同 UUID;不泄露敏感信息 | MD5 算法存在碰撞风险 | hhhhhhhh-hhhh-3hhh-hhhh-hhhhhhhhhhhh |
| UUID v4 | 随机生成 | 使用随机或伪随机数生成 | 简单;不泄露任何信息 | 理论上存在碰撞可能性 | rrrrrrrr-rrrr-4rrr-rrrr-rrrrrrrrrrrr |
| UUID v5 | 基于名字的 SHA-1 版本 | 使用 SHA-1 哈希算法和命名空间 | 相同输入产生相同 UUID;比 v3 更安全 | 生成速度比 v4 慢 | hhhhhhhh-hhhh-5hhh-hhhh-hhhhhhhhhhhh |
| UUID v6 | 可排序、随机化的时间版本 | 基于时间戳,但改进了 v1 的缺点 | 可排序;随机性更好;不泄露 MAC 地址 | 较新,支持可能不够广泛 | tttttttt-tttt-6ttt-rrrr-rrrrrrrrrrrr |
| UUID v7 | 基于 Unix 时间戳的版本 | 使用 Unix 时间戳和随机数 | 可排序;易于生成和解析 | 较新,支持可能不够广泛 | ttttttt-tttt-7rrr-rrrr-rrrrrrrrrrrr |
| UUID v8 | 自定义版本 | 允许用户自定义生成算法 | 灵活;可满足特定需求 | 较新,支持可能不够广泛;可能不兼容 | xxxxxxxx-xxxx-8xxx-xxxx-xxxxxxxxxxxx |
请注意,这些 ASCII 格式是简化的表示,实际的 UUID 字符串会包含 32 个十六进制字符,由连字符分隔成 5 组. 版本号总是出现在第三组的第一个字符位置.
UUIDv6 是 UUIDv1 的字段兼容版本,重新排序以改善数据库局部性. 预计 UUIDv6 将主要在使用 UUIDv1 的环境中实现. 系统不涉及旧版 UUIDv1 的应该使用 UUIDv7.
UUID v1 的格式:
- 时间戳部分:占用 60 位,表示自 1582 年 10 月 15 日以来的 100 纳秒间隔.
- 低位时间戳(32 位)
- 中位时间戳(16 位)
- 高位时间戳和版本号(4 位版本号 + 12 位高位时间戳)
- 时钟序列:占用 14 位,用于防止时钟回拨导致的重复.
- 时钟序列高位(6 位)
- 时钟序列低位(8 位)
- 节点:占用 48 位,通常是设备的 MAC 地址.
不像 UUIDv1 将时间戳分为低、中、高的部分,UUIDv6 改变了这个序列,因此时间戳字节从最高位到最低位存储. 也就是说,给定一个 60 位时间戳,对于 UUIDv6,首先存储前 48 个最高有效位,然后存储 4 位版本,后面跟着原始的 60 位时间戳剩余的 12 位. 时钟序列和节点位保持不变:
UUID v6 的格式:
- 前
48位:时间戳的高位部分(32位)和中位部分(16位). - 接下来的
16位:包含版本号(4位,固定为6)和时间戳的低位部分(12位). - 接下来的
16位:时钟序列(14位)和变体(2位). - 最后
48位:节点(通常是MAC地址).
这就非常适合根据时间戳进行排序了,对不?
UUIDv7 的特点是采用了一个基于时间排序的值字段,这个字段源自广泛实施且众所周知的 Unix 纪元时间戳,即从 1970 年 1 月 1 日 UTC 零点开始计算的毫秒数,不包括闰秒.
总的来说 UUIDv7 相比 UUIDv1 和 UUIDv6 具有更好的熵特性,主要体现在以下几个方面:
- 随机性增强:UUIDv7 在时间戳之外的部分引入了更多的随机性. 这意味着即使在同一毫秒内生成多个 UUID,它们之间也会有更大的差异,从而减少了碰撞的可能性.
- 排序特性:UUIDv7 保留了时间戳的排序特性,这使得它在需要按时间顺序排序的应用中非常有用. 同时,随机部分的引入不会影响这种排序特性.
- 避免硬件依赖:UUIDv1 和 UUIDv6 通常依赖于设备的 MAC 地址来生成节点部分,这可能会带来隐私和安全问题. UUIDv7 通过使用随机数来替代这种依赖,增强了安全性和隐私保护.
- 更高的熵:由于引入了更多的随机位,UUIDv7 的熵更高. 这意味着在相同的时间间隔内,UUIDv7 可以生成更多的唯一标识符.
UUIDv7 的值生成方式如下:将 Unix 时间戳(以毫秒为单位)分配到最高有效位的 48 位中,然后在剩余的 74 位中(不包括必要的版本和变体位)填充随机位. 这样每生成一个新的 UUIDv7 时都能保证其唯一性,另外,为了在一毫秒内保证额外的单调性,实现方案可以选择用以下子字段的组合来共同填充这 74 位,顺序从最高有效位到最低有效位排列:
-
可选的亚毫秒级时间戳分数(最多
12位). -
可选的经过精心设计的计数器.
-
对于任何剩余空间,为每个新生成的
UUIDv7填充随机数据.
UUID v7的格式:
- 时间戳部分:占用 48 位,表示自 Unix 纪元(1970 年 1 月 1 日)以来的毫秒数.
- 32 位的高位时间戳
- 16 位的低位时间戳
- 版本号:占用 4 位,固定为 7.
- 随机部分:占用 74 位,用于增加熵和唯一性.
https://antonz.org/uuidv7/ 这个网站提供了 33 种编程语言的 UUID v7 版本的实现,比如 Go 语言的实现:
package main
import (
"crypto/rand"
"fmt"
"math/big"
"time"
)
func uuidv7() ([16]byte, error) {
// 随机数组
var value [16]byte
_, err := rand.Read(value[:])
if err != nil {
return value, err
}
// 当前的时间戳,单位毫秒
timestamp := big.NewInt(time.Now().UnixMilli())
// 填充时间戳的高位
timestamp.FillBytes(value[0:6])
// 设置版本号
value[6] = (value[6] & 0x0F) | 0x70
// 设置变种
value[8] = (value[8] & 0x3F) | 0x80
return value, nil
}
func main() {
for i := 0; i < 100; i++ {
uuidVal, _ := uuidv7()
fmt.Printf("%x\n", uuidVal)
}
}输出结果:
01929a3a26077b4a959d912d6b57bcee
01929a3a2607704f94dcaf64e8c6927a
01929a3a26077c3fb570e869577cd377
01929a3a26077b57b3949f5754157f59
......事实上,我们使用最广泛的 Go 库 google/uuid 已经支持了 version 7. 其中uuid.New()生成的 ID 是版本 version 4 的 UUID.
写一个程序,输出 100 个 UUIDv7 的 ID 和时间戳:
package main
import (
"fmt"
"github.com/google/uuid"
)
func main() {
for i := 0; i < 4; i++ {
id, _ := uuid.NewV7()
fmt.Println(id, id.Time())
}
}输出:
01929a3c-387b-7c33-8b21-d47bea7c1768 139484572908750000
01929a3c-387b-78de-858c-c2f8f3985a03 139484572908750000
01929a3c-387b-7a39-b657-223cb8da83f3 139484572908750000
01929a3c-387b-7a58-9962-c572a73e3359 139484572908750000
......当然了,我们自己实现一个 UUID Version 7,这里实现十六进制打印展示:
package main
import (
"crypto/rand"
"encoding/binary"
"fmt"
"time"
)
func main() {
uuid := make([]byte, 16)
makeV7(uuid)
fmt.Printf("Generated UUID v7: %x\n", uuid)
}
func makeV7(uuid []byte) {
_ = uuid[15] // bounds check
t, s := getV7Time() // 返回毫秒数和序列号
// 填充前48bit的时间戳
uuid[0] = byte(t >> 40)
uuid[1] = byte(t >> 32)
uuid[2] = byte(t >> 24)
uuid[3] = byte(t >> 16)
uuid[4] = byte(t >> 8)
uuid[5] = byte(t)
uuid[6] = 0x70 | (0x0F & byte(s>>8)) // 设置版本号7以及后四位存储序列号的前四位
uuid[7] = byte(s) // 存储序列号的后八位
// 剩余的 uuid[8] ~ uuid[15] 位已填充随机数
rand.Read(uuid[8:])
}
// 返回毫秒数和序列号
func getV7Time() (uint64, uint16) {
now := time.Now().UnixMilli()
seq := make([]byte, 2)
rand.Read(seq)
sequence := binary.BigEndian.Uint16(seq)
return uint64(now), sequence
}