Skip to content

Latest commit

 

History

History
385 lines (279 loc) · 12.3 KB

File metadata and controls

385 lines (279 loc) · 12.3 KB

UUID 和 UUID v7

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 具有以下特点:

  1. 唯一性: 理论上, UUID 的重复概率极低, 可以认为是唯一的.

  2. 分布式系统友好: 不需要中央协调即可生成, 适用于分布式系统.

  3. 跨平台: 大多数编程语言和数据库系统都支持 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).

UUID 的版本


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 的生成方法,可以说是煞费苦心:

  1. ULID

  2. LexicalUUID

  3. Snowflake

  4. Flake

  5. ShardingID

  6. KSUID

  7. Elasticflake

  8. FlakeID

  9. Sonyflake

  10. orderedUuid

  11. COMBGUID

  12. SID

  13. pushID

  14. XID

  15. ObjectID

  16. 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 组. 版本号总是出现在第三组的第一个字符位置.

UUID version 7


UUIDv6UUIDv1 的字段兼容版本,重新排序以改善数据库局部性. 预计 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 纪元时间戳,即从 197011UTC 零点开始计算的毫秒数,不包括闰秒.

总的来说 UUIDv7 相比 UUIDv1UUIDv6 具有更好的熵特性,主要体现在以下几个方面:

  1. 随机性增强:UUIDv7 在时间戳之外的部分引入了更多的随机性. 这意味着即使在同一毫秒内生成多个 UUID,它们之间也会有更大的差异,从而减少了碰撞的可能性.
  2. 排序特性:UUIDv7 保留了时间戳的排序特性,这使得它在需要按时间顺序排序的应用中非常有用. 同时,随机部分的引入不会影响这种排序特性.
  3. 避免硬件依赖:UUIDv1 和 UUIDv6 通常依赖于设备的 MAC 地址来生成节点部分,这可能会带来隐私和安全问题. UUIDv7 通过使用随机数来替代这种依赖,增强了安全性和隐私保护.
  4. 更高的熵:由于引入了更多的随机位,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 4UUID.

写一个程序,输出 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
}