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

相关推荐
咖啡の猫28 分钟前
数据库的基本概念
数据库
无极低码1 小时前
FLASK和GPU依赖安装
后端·python·flask
小卓笔记1 小时前
keepalived应用
linux·服务器·数据库
星际编程喵1 小时前
Flask实时监控:打造智能多设备在线离线检测平台(升级版)
后端·python·单片机·嵌入式硬件·物联网·flask
钢铁男儿2 小时前
Python 生成数据(随机漫步)
开发语言·python·信息可视化
八股文领域大手子3 小时前
Leetcode32 最长有效括号深度解析
java·数据库·redis·sql·mysql
正经教主3 小时前
【菜鸟飞】在vsCode中安装python的ollama包出错的问题
开发语言·人工智能·vscode·python·ai·编辑器
Dongliner~3 小时前
【QT:多线程、锁】
开发语言·qt
鹏神丶明月天3 小时前
mybatis_plus的乐观锁
java·开发语言·数据库
极客代码3 小时前
Unix 域套接字(本地套接字)
linux·c语言·开发语言·unix·socket·unix域套接字·本地套接字