自学内容网 自学内容网

Go 语言 UUID 库 google/uuid 源码解析:UUID version7 的实现

google/uuid 库地址

建议阅读内容

在阅读此篇文章之前,建议先了解 UUIDv1 的构成、UUIDv4 的 API 以及掌握位运算。

了解 UUIDv1 的构成可以参考Go 语言 UUID 库 google/uuid 源码解析:UUID version1 的实现RFC 9562

了解 UUIDv4 的 API 可以看Go 语言 UUID 库 google/uuid 源码解析:UUID version4 的实现

位运算可以参考详解位运算(&、|、、&、>>、<<)

相较于 UUIDv1,UUIDv7 的改进

UUIDv7 是 UUIDv1 的优化版本,其优化有三点:

  1. 使用自 1970 年 1 月 1 日午夜(Unix 纪元时间戳源)以来的毫秒数代替自 1582 年 10 月 15 日以来的 100 纳秒数作为时间戳。

  2. UUIDv7 在序列中保持时间戳的顺序(UUIDv1 会对时间戳进行重排),这意味着生成的 UUID 会按时间顺序排列。优化在数据库中作为索引时的性能表现。

  3. 随机生成序列中的 74 位(UUID 总共 128 位),增加熵特性,减少逆向推导的可能性(UUIDv1 包含 MAC 地址)。

UUIDv7 的结构介绍

UUIDv7 主要由三部分组成(以下陈述并没有按顺序排列):

  1. 在最高的 48 位分配的Unix时间戳。

  2. 6 位标志位(2 位变体标识,4位版本标识)。

  3. 以及随机填充的74位。

UUIDv7 具体的字段和位具体布局如下:

(表格顶部的两行数字用于表示位数,00,01,…,10,11,…,20,21,…,30,31)

 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 中

type uuid [16]byte

uuid[0:5]: 时间戳
uuid[6] 高 4 位:版本号
uuid[6] 低 4 位 与 uuid[7]: 随机值part1
uuid[8:15]:随机值part2

具有 v7 特色的时间戳的制作

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 具体的字段和位具体布局如下:

 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 的位置已经是最终值。

序列生成第二步:填充时间戳与版本号

// 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 位)。

序列生成第三步:整合调用

func NewV7() (UUID, error) {
uuid, err := NewRandom()
if err != nil {
return uuid, err
}
makeV7(uuid[:])
return uuid, nil
}

整合步骤如下:

  1. 调用 NewRandom 填充随机值,使用 uuid 接收返回的 UUIDv4
  2. 检查错误
  3. 调用 makeV7 填充时间戳和版本号
  4. 返回 UUIDv7

完整函数调用关系图

在这里插入图片描述

到这里一个完整的 UUIDv7 便完成了。

以上就是 UUIDv7 实现的所有内容,希望你能有所收获。

参考资料

RFC 9562
uuid package


原文地址:https://blog.csdn.net/a2025834646/article/details/140441251

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!