UUIDV7: 我就是要用UUID做主键
一直以来的互联网谣言: UUID是不适合用作数据库主键的.
为什么? 因为UUID是全随机的, 对于数据库的索引不友好, 插入时可能导致大量的索引树的分支合并.
UUID
根据RFC4122的描述, UUID被设计用于去中心化的ID生成
格式
128bit, 16个字节, 示例: f81d4fae-7dec-11d0-a765-00a0c91e6bf6
按照8-4-4-4-12的格式进行分配, 每个位置用其16进制编码表示
具体结构如下:
+-------------------------------+
| 128 bits |
+-------------------------------+
| time_low (32 bits) | 0 - 31
+-------------------------------+
| time_mid (16 bits) | 32 - 47
+-------------------------------+
| time_hi_and_version (16 bits) | 48 - 63
+-------------------------------+
| clock_seq_hi_and_reserved (8 bits) | 64 - 71
+-------------------------------+
| clock_seq_low (8 bits) | 72 - 79
+-------------------------------+
| node (48 bits) | 80 - 127
+-------------------------------+
- time_low: 32位(8个十六进制数字)
- time_mid: 16位(4个十六进制数字)
- time_hi_and_version: 16位(4个十六进制数字,其中前4位表示版本号)
- clock_seq_and_reserved: 16位(4个十六进制数字,其中前2位用于标识变体)
- node: 48位(12个十六进制数字,通常是MAC地址或随机生成的值)
实现
在提案中, 同样提及了三种方式生成UUID
-
基于时间的UUID(Version 1) :
-
这种UUID是基于当前的时间戳生成的,时间戳是从1582年10月15日(公历改革的日期)开始的100纳秒间隔计数。
-
它包含一个节点标识符(通常是设备的MAC地址)以及一个时钟序列,以避免因时钟回拨而导致的重复UUID。
-
生成过程如下:
- 获取当前时间的100纳秒间隔计数。
- 获取当前的节点ID。
- 生成一个时钟序列,如果时钟回拨或节点ID改变,则更新时钟序列。
- 将这些值格式化为UUID的各个部分。
-
-
基于名称的UUID(Version 3和Version 5) :
-
这种UUID是通过对名称(如URL、域名等)进行哈希计算生成的。
-
Version 3使用MD5哈希算法,而Version 5使用SHA-1哈希算法。
-
生成过程如下:
- 为名称分配一个命名空间ID(UUID)。
- 将名称转换为标准的八位字节序列,并将命名空间ID以网络字节顺序放置。
- 计算命名空间ID与名称的哈希值。
- 根据哈希值的特定部分设置UUID的各个字段,包括版本号和时钟序列。
-
-
随机或伪随机UUID(Version 4) :
-
这种UUID是基于随机或伪随机数生成的。
-
生成过程如下:
- 设置UUID的变体字段(clock_seq_hi_and_reserved)和版本字段(time_hi_and_version)。
- 将UUID的其他位设置为随机或伪随机生成的值。
- 生成的UUID包括了当前的变体和版本信息。
-
完全随机的V4
其中, V3或者V5使用场景不是很多, V4的使用是最广泛的. 因为其实现方式, 决定了他的随机性是很强的, 几乎不存在空间上的内聚性.
存在时序性的V1
实际上V1已经表现出了时序性, 也就是在时间上的递增性.
但是为什么UUIDV1没有被广泛使用呢?
-
隐私问题:
- UUIDv1包含了节点标识符,通常是设备的MAC地址。这意味着生成的UUID可以泄露生成UUID的设备的信息,导致隐私问题。例如,攻击者可以通过UUID推测出特定设备的身份或位置。
-
时钟回拨问题:
- UUIDv1依赖系统时间来生成UUID,如果系统的时钟被意外地设置回过去,可能会导致生成重复的UUID。这是一个潜在的冲突风险,尤其是在分布式系统中,多个节点可能会同时生成UUID。
-
跨设备的唯一性问题:
- 随着设备移动或更换网络,MAC地址可能会发生变化。这使得UUIDv1在某些情况下可能无法保证跨设备的一致性和唯一性。
-
生成效率:
- UUIDv1生成过程需要访问系统时间和节点ID,可能会比其他方法(如UUIDv4)略显复杂且效率低。
-
替代方案的可用性:
- UUIDv4(基于随机或伪随机数生成的UUID)提供了更大的随机性和隐私保护,不需要依赖于时间或设备的标识符,因而更受欢迎。UUIDv4也避免了UUIDv1存在的许多缺点。
UUID V6 V7
为了解决UUID的一些问题, UUIDV6, UUIDV7 都尝试了进行改进, 参考文档
UUID V6
UUIDv6 相比于 UUIDv1 进行了以下改进:
-
时间顺序排序:
- UUIDv6 对时间戳的字节顺序进行了重新排列,使其变得可排序。UUIDv1 的时间戳是按照高位、中位和低位的顺序存储,这导致在数据库中插入时新生成的UUID可能分散在索引的随机位置,影响了数据库性能。UUIDv6 将时间戳的字节顺序调整为从高到低存储,确保相邻生成的UUID在数据库中也保持相近。
-
简化实现:
- UUIDv6 的设计使得现有的 UUIDv1 实现可以相对简单地调整为支持 UUIDv6,避免了完全重新设计的复杂性。
-
消除隐私问题:
- UUIDv1 使用 MAC 地址作为节点部分,这可能引发隐私和安全问题。虽然UUIDv6在节点部分仍然可以使用伪随机值,但在实现上可以选择避免使用MAC地址,从而降低安全风险。
-
改进的时钟序列处理:
- UUIDv6 保留了UUIDv1中的时钟序列,但在生成时提供了更好的保证,确保即使在同一时间戳下生成多个UUID,仍然能保持单调性和唯一性。
-
不再依赖于特定的时间戳格式:
- UUIDv1 使用了不太常见的100纳秒公历纪元时间戳,UUIDv6 的时间戳设计更为灵活,可以根据需要采用不同的时间戳格式。
UUIDV7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unix_ts_ms (48 bits) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ver (4 bits) | rand_a (12 bits) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| var (2 bits) | rand_b (62 bits) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- 基于 Unix Epoch 时间戳:
UUIDv7 使用 Unix Epoch 时间戳(自 1970 年 1 月 1 日以来的毫秒数)作为时间源。这种时间戳更为常见和易于理解,而 UUIDv1 使用的是不常见的 Gregorian 时间戳(以 100 纳秒为单位的计时),这使得其在处理和表示上更加复杂。 - 排序性:
UUIDv7 设计为时间有序,可以直接按字节比较进行排序。由于它的时间戳是以毫秒为单位的,因此在数据库索引中的插入顺序更为合理,可以提高插入性能,而 UUIDv1 和 UUIDv6 的时间戳分散在不同的字段中,导致在数据库中插入时索引局部性较差。 - 改进的熵特性:
UUIDv7 提供了更好的熵特性,使用了额外的随机数位(rand_a 和 rand_b)来增加唯一性。这使得 UUIDv7 在高并发情况下生成的 UUID 更加独特,降低了碰撞的可能性。 - 简化的结构:
UUIDv7 的结构比 UUIDv1 和 UUIDv6 更加简单,专注于提供易于实现的时间戳和随机数据的组合。UUIDv7 包含一个48位的 Unix 时间戳,后面跟着一部分版本和随机数据,整体结构更加清晰。 - 去除了 MAC 地址:
UUIDv1 包含了设备的 MAC 地址,这在隐私和安全性上带来了风险。UUIDv7 不再使用 MAC 地址,改为完全依赖随机数生成器和时间戳,从而提高了安全性。
可以看到UUIDV7的结构已经具有了相当的时间内聚性, 即在一段时间内, UUID的大小总是在某个范围内, 这其实已经满足了我们对于主键的要求. 而且一共74bit的随机位也保证了非常好的随机性.
在增加上主键的唯一性保证, 其实已经基本满足了我们对于主键的要求.