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)

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
}

整合步骤如下:

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

完整函数调用关系图

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

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

参考资料

RFC 9562
uuid package

相关推荐
woshilys10 分钟前
sql server 查询对象的修改时间
运维·数据库·sqlserver
Hacker_LaoYi11 分钟前
SQL注入的那些面试题总结
数据库·sql
2401_857439691 小时前
SSM 架构下 Vue 电脑测评系统:为电脑性能评估赋能
开发语言·php
建投数据1 小时前
建投数据与腾讯云数据库TDSQL完成产品兼容性互认证
数据库·腾讯云
SoraLuna2 小时前
「Mac畅玩鸿蒙与硬件47」UI互动应用篇24 - 虚拟音乐控制台
开发语言·macos·ui·华为·harmonyos
向前看-2 小时前
验证码机制
前端·后端
xlsw_2 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
Hacker_LaoYi2 小时前
【渗透技术总结】SQL手工注入总结
数据库·sql
岁月变迁呀2 小时前
Redis梳理
数据库·redis·缓存
独行soc2 小时前
#渗透测试#漏洞挖掘#红蓝攻防#护网#sql注入介绍06-基于子查询的SQL注入(Subquery-Based SQL Injection)
数据库·sql·安全·web安全·漏洞挖掘·hw