建议阅读内容
在阅读此篇文章之前,建议先了解 UUIDv1 的构成、UUIDv4 的 API 以及掌握位运算。
了解 UUIDv1 的构成可以参考Go 语言 UUID 库 google/uuid 源码解析:UUID version1 的实现 或 RFC 9562。
了解 UUIDv4 的 API 可以看Go 语言 UUID 库 google/uuid 源码解析:UUID version4 的实现。
位运算可以参考详解位运算(&、|、^、&^、>>、<<)。
相较于 UUIDv1,UUIDv7 的改进
UUIDv7 是 UUIDv1 的优化版本,其优化有三点:
-
使用自 1970 年 1 月 1 日午夜(Unix 纪元时间戳源)以来的毫秒数代替自 1582 年 10 月 15 日以来的 100 纳秒数作为时间戳。
-
UUIDv7 在序列中保持时间戳的顺序(UUIDv1 会对时间戳进行重排),这意味着生成的 UUID 会按时间顺序排列。优化在数据库中作为索引时的性能表现。
-
随机生成序列中的 74 位(UUID 总共 128 位),增加熵特性,减少逆向推导的可能性(UUIDv1 包含 MAC 地址)。
UUIDv7 的结构介绍
UUIDv7 主要由三部分组成(以下陈述并没有按顺序排列):
-
在最高的 48 位分配的Unix时间戳。
-
6 位标志位(2 位变体标识,4位版本标识)。
-
以及随机填充的74位。
UUIDv7 具体的字段和位具体布局如下:
(表格顶部的两行数字用于表示位数,00,01,...,10,11,...,20,21,...,30,31)
go
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unix_ts_ms |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unix_ts_ms | ver | rand_a (12 bit seq) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var| rand_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| rand_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
实现
UUID 存储在 type uuid 中
go
type uuid [16]byte
uuid[0:5]: 时间戳
uuid[6] 高 4 位:版本号
uuid[6] 低 4 位 与 uuid[7]: 随机值part1
uuid[8:15]:随机值part2
具有 v7 特色的时间戳的制作
go
var lastV7time int64
const nanoPerMilli = 1000000
// getV7Time 返回毫秒和纳秒 / 256 的时间。
// 返回的 (milli << 12 + seq) 保证大于
// 任何之前对 getV7Time 的调用返回的 (milli << 12 + seq)。
func getV7Time() (milli, seq int64) {
timeMu.Lock()
defer timeMu.Unlock()
nano := timeNow().UnixNano()
milli = nano / nanoPerMilli
// 序列号在 0 到 3906 之间(nanoPerMilli>>8)
seq = (nano - milli*nanoPerMilli) >> 8
now := milli<<12 + seq
if now <= lastV7time {
now = lastV7time + 1
milli = now >> 12
seq = now & 0xfff
}
lastV7time = now
return milli, seq
}
函数 getV7Time 实际上生成两部分内容:milli 和 seq。
milli 就是时间戳,而 seq 是随机值part1。
milli 就是通过 time.Now().UnixNano() 获取的 Unix 纪元到当前时间的纳秒数 /
1x10^6 次方得到的。(因为1毫秒 = 1x10^6纳秒)。需要注意的是,/
会导致纳秒部分精度丢失。
seq 则等于 (nano - milli * nanoPerMilli) >> 8,因为 milli 在进行 /
时导致纳秒精度丢失,所以 nano - milli * nanoPerMilli 的结果就是丢失的纳秒数,>>8
等于除以 256。因为丢失的纳秒数徘徊在 0~999999 之间,所以 seq 的值在 0 ~ 3906 之间。
同时,我们还会记录上次生成的时间信息(milli << 12 + seq),通过比较上次的时间信息和当前的时间信息,判断其是否保持递增,如果没有递增则在上次的时间信息基础上再重新计算 milli 和 seq。
序列生成第一步:填充随机值
UUIDv7 的生成是从向 uuid 的所有位中填充随机值开始的,然后再将对应位置变成正确的内容。而填充随机值的方式便是使用 UUIDv4 的 API:NewRandom 或者 NewRandomFromReader,简单说就是将 uuid(16bytes) 的 128 位随机填满。
注:UUIDv4 生成的时候也会填充版本号和变体号,因为 UUIDv7 版本号和 UUIDv4 不同,所以会覆盖版本号,但是变体号并不会被覆盖,所以后续不再填充变体号。
UUIDv4 具体的字段和位具体布局如下:
go
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| random_a |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| random_a | ver | random_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var| random_c |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| random_c |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
后续过程会在 random_a 的位置填充 milli,在 random_b 的位置填充 seq,而 var 和 random_c 的位置已经是最终值。
序列生成第二步:填充时间戳与版本号
go
// makeV7 填充 48 位时间(uuid[0] - uuid[5]),设置版本 b0111(uuid[6])
func makeV7(uuid []byte) {
_ = uuid[15] // 边界检查
t, s := getV7Time()
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))
uuid[7] = byte(s)
}
理解这段代码,需要知道 UUIDv7 的时间戳部分只有 48 位,所以会从 t 中截取 48 位放置到 UUID 的高 48 位中。byte() 的用途是截取其低 8 位进行填充,>>
操作的目的是将高位移到低位,如 uuid[0] = byte(t >> 40)
就是将 t 右移 40 位,然后截取当前低 8 位放置到 uuid[0] 中。
而 0x70 | (0x0F & byte(s>>8))
的作用是在 uuid[6] 中同时设置版本号(高 4 位为 0111)和随机值part1的一部分(低 4 位)。
序列生成第三步:整合调用
go
func NewV7() (UUID, error) {
uuid, err := NewRandom()
if err != nil {
return uuid, err
}
makeV7(uuid[:])
return uuid, nil
}
整合步骤如下:
- 调用 NewRandom 填充随机值,使用 uuid 接收返回的 UUIDv4
- 检查错误
- 调用 makeV7 填充时间戳和版本号
- 返回 UUIDv7
完整函数调用关系图
到这里一个完整的 UUIDv7 便完成了。
以上就是 UUIDv7 实现的所有内容,希望你能有所收获。