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

  1. 基于时间的UUID(Version 1)

    • 这种UUID是基于当前的时间戳生成的,时间戳是从1582年10月15日(公历改革的日期)开始的100纳秒间隔计数。

    • 它包含一个节点标识符(通常是设备的MAC地址)以及一个时钟序列,以避免因时钟回拨而导致的重复UUID。

    • 生成过程如下:

      1. 获取当前时间的100纳秒间隔计数。
      2. 获取当前的节点ID。
      3. 生成一个时钟序列,如果时钟回拨或节点ID改变,则更新时钟序列。
      4. 将这些值格式化为UUID的各个部分。
  2. 基于名称的UUID(Version 3和Version 5)

    • 这种UUID是通过对名称(如URL、域名等)进行哈希计算生成的。

    • Version 3使用MD5哈希算法,而Version 5使用SHA-1哈希算法。

    • 生成过程如下:

      1. 为名称分配一个命名空间ID(UUID)。
      2. 将名称转换为标准的八位字节序列,并将命名空间ID以网络字节顺序放置。
      3. 计算命名空间ID与名称的哈希值。
      4. 根据哈希值的特定部分设置UUID的各个字段,包括版本号和时钟序列。
  3. 随机或伪随机UUID(Version 4)

    • 这种UUID是基于随机或伪随机数生成的。

    • 生成过程如下:

      1. 设置UUID的变体字段(clock_seq_hi_and_reserved)和版本字段(time_hi_and_version)。
      2. 将UUID的其他位设置为随机或伪随机生成的值。
      3. 生成的UUID包括了当前的变体和版本信息。

完全随机的V4

其中, V3或者V5使用场景不是很多, V4的使用是最广泛的. 因为其实现方式, 决定了他的随机性是很强的, 几乎不存在空间上的内聚性.

存在时序性的V1

实际上V1已经表现出了时序性, 也就是在时间上的递增性.

但是为什么UUIDV1没有被广泛使用呢?

  1. 隐私问题

    • UUIDv1包含了节点标识符,通常是设备的MAC地址。这意味着生成的UUID可以泄露生成UUID的设备的信息,导致隐私问题。例如,攻击者可以通过UUID推测出特定设备的身份或位置。
  2. 时钟回拨问题

    • UUIDv1依赖系统时间来生成UUID,如果系统的时钟被意外地设置回过去,可能会导致生成重复的UUID。这是一个潜在的冲突风险,尤其是在分布式系统中,多个节点可能会同时生成UUID。
  3. 跨设备的唯一性问题

    • 随着设备移动或更换网络,MAC地址可能会发生变化。这使得UUIDv1在某些情况下可能无法保证跨设备的一致性和唯一性。
  4. 生成效率

    • UUIDv1生成过程需要访问系统时间和节点ID,可能会比其他方法(如UUIDv4)略显复杂且效率低。
  5. 替代方案的可用性

    • UUIDv4(基于随机或伪随机数生成的UUID)提供了更大的随机性和隐私保护,不需要依赖于时间或设备的标识符,因而更受欢迎。UUIDv4也避免了UUIDv1存在的许多缺点。

UUID V6 V7

为了解决UUID的一些问题, UUIDV6, UUIDV7 都尝试了进行改进, 参考文档

UUID V6

UUIDv6 相比于 UUIDv1 进行了以下改进:

  1. 时间顺序排序

    • UUIDv6 对时间戳的字节顺序进行了重新排列,使其变得可排序。UUIDv1 的时间戳是按照高位、中位和低位的顺序存储,这导致在数据库中插入时新生成的UUID可能分散在索引的随机位置,影响了数据库性能。UUIDv6 将时间戳的字节顺序调整为从高到低存储,确保相邻生成的UUID在数据库中也保持相近。
  2. 简化实现

    • UUIDv6 的设计使得现有的 UUIDv1 实现可以相对简单地调整为支持 UUIDv6,避免了完全重新设计的复杂性。
  3. 消除隐私问题

    • UUIDv1 使用 MAC 地址作为节点部分,这可能引发隐私和安全问题。虽然UUIDv6在节点部分仍然可以使用伪随机值,但在实现上可以选择避免使用MAC地址,从而降低安全风险。
  4. 改进的时钟序列处理

    • UUIDv6 保留了UUIDv1中的时钟序列,但在生成时提供了更好的保证,确保即使在同一时间戳下生成多个UUID,仍然能保持单调性和唯一性。
  5. 不再依赖于特定的时间戳格式

    • UUIDv1 使用了不太常见的100纳秒公历纪元时间戳,UUIDv6 的时间戳设计更为灵活,可以根据需要采用不同的时间戳格式。

UUIDV7

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       unix_ts_ms (48 bits)                       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  ver (4 bits)  |         rand_a (12 bits)                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| var (2 bits)   |                rand_b (62 bits)                 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  1. 基于 Unix Epoch 时间戳
    UUIDv7 使用 Unix Epoch 时间戳(自 1970 年 1 月 1 日以来的毫秒数)作为时间源。这种时间戳更为常见和易于理解,而 UUIDv1 使用的是不常见的 Gregorian 时间戳(以 100 纳秒为单位的计时),这使得其在处理和表示上更加复杂。
  2. 排序性
    UUIDv7 设计为时间有序,可以直接按字节比较进行排序。由于它的时间戳是以毫秒为单位的,因此在数据库索引中的插入顺序更为合理,可以提高插入性能,而 UUIDv1 和 UUIDv6 的时间戳分散在不同的字段中,导致在数据库中插入时索引局部性较差。
  3. 改进的熵特性
    UUIDv7 提供了更好的熵特性,使用了额外的随机数位(rand_a 和 rand_b)来增加唯一性。这使得 UUIDv7 在高并发情况下生成的 UUID 更加独特,降低了碰撞的可能性。
  4. 简化的结构
    UUIDv7 的结构比 UUIDv1 和 UUIDv6 更加简单,专注于提供易于实现的时间戳和随机数据的组合。UUIDv7 包含一个48位的 Unix 时间戳,后面跟着一部分版本和随机数据,整体结构更加清晰。
  5. 去除了 MAC 地址
    UUIDv1 包含了设备的 MAC 地址,这在隐私和安全性上带来了风险。UUIDv7 不再使用 MAC 地址,改为完全依赖随机数生成器和时间戳,从而提高了安全性。

可以看到UUIDV7的结构已经具有了相当的时间内聚性, 即在一段时间内, UUID的大小总是在某个范围内, 这其实已经满足了我们对于主键的要求. 而且一共74bit的随机位也保证了非常好的随机性.

在增加上主键的唯一性保证, 其实已经基本满足了我们对于主键的要求.