UUID

UUID 是最经典的唯一 ID,号称「绝对不重复」。一个典型的 UUID 表示为 8246dba2-3737-4067-83f5-f2a8dd0fb2e8

介绍

UUID 事实上是一个 16 字节(128 位二进制)的编码,常见的文本格式只是一种 “base16“ 编码(利用 0-9a-f 共 26 种字符编码而成、大小写不敏感。UUID 的文本表示始终为 8-4-4-4-12 共计32 个字母数字 + 4 个连字符。

二进制表示 → 字符表示的转换

每个字母/数字代表了 4-bit,文本表示就是每部分(按上面所述,分隔成了 5 个部分分别转换)直接的二进制 → 十六进制的转换(可参考代码

UUID 版本

UUID 目前有 8 个版本,包括由 RFC4122 定义的 UUID v1-v5 和它的计划修正草案定义的 UUID v6-v8。

你通常所见到的 UUID 应当是 v4 或者 v7 版本,这两个版本可以「开箱即用」,不传递任何参数来生成一个唯一值

nil UID

这其实不是一个「版本」,只是规范规定,所有位都是 0 的 UUID 即为 nil UUID,即空值

UUID v1

v1 版本利用当前机器的 MAC + 时间戳组成

  • 时间戳 使用的是 60bit 的,是当前时间(UTC 时间)距离 1582 年 10 月 15 日 0 时所过去的 100ns 数(这个日期是 Gregorian reform to the Christian calendar

  • 版本 4bit 的版本信息,对于 v1 而言,始终固定为 0001 (文本表示为 1)

  • 时钟序列与保留 16bit(0-5 为单调时钟高位,6-7 固定为 01,8-15 为单调时钟低位)

    • 单调时钟:一个单调递增的数字,与实际时间无关

  • MAC 地址 使用的是 48bit

    • 注:事实上现在的库都不会直接使用 MAC 地址了

v1 版本存在的问题:

  • 会暴露生成 UUID 的机器 ID 与时间

    • 甚至规范中直接暴露了 MAC 地址(尽管事实上现在的库都不会直接使用 MAC 地址了)

    • 机器 ID 一致,id 会变得可预测

  • 极端情况下同一台机器发生时光倒流时存在一定可能生成相同的 UUID(尽管可能性很小,因为除了 walltime 还使用了单调时钟来保证了绝大多数情况下时光倒流不出问题)

UUID v2

v2 是一个模糊的规范,是一个「DCE 安全」的版本,但是规范中并没有很明确的说明它,市面上的库也基本没有对他的实现,可以认为它不存在而直接忽略

UUID v3

v3 版本不再使用时间戳,而是使用 namespace + name 来生成

流程:

  1. 计算出 namespace + name(字节连接)的 MD5 Hash(16 字节 128 bit),直接赋值给 UUID

    • 规范里的 namespace 也是 UUID,计算 md5 时使用它的 16Bytes 表示形式

    • 规范里的 name 可以使用任何文本字符串(需要可以转换为 bytes,其他文本编码的需要自行定义转换方式)

  2. 修改这个 UUID 的 60-63bit 为 0011 (v3 固定的版本号)

  3. 修改这个 UUID 的 70-71bit 为 01 (固定的保留值)

 

重点:

  • 和时间完全无关(无论是 walltime 还是 monotime 等),只要是相同的 namespace 和相同的 name 都会生成相同的 uuid

  • RFC 4122 的 Appendix C 定义了几个 namespace(DNS, URL, OID, X500)作为约定

UUID v5

之所以先不说 v4 而是说 v5 是因为它实在和 v3 太像了😂

唯一的区别:哈希算法使用 SHA-1 而不是 MD5(规范里面也明确说明了只有当为了历史兼容不得不使用 MD5 时才应使用,其他情况都建议使用 SHA-1)

SHA-1 是 20 bytes 160bit,第一步计算出 hash 赋值时只使用前 16 字节 128 bit,另外版本号使用 0101 (v5 固定的版本号)

UUID v4

v4 也很简单粗暴:随机生成

  1. 随机生成 16bytes(128bit),直接赋值给 UUID

  2. 修改这个 UUID 的 60-63bit 为 0100 (固定的版本号)

  3. 修改这个 UUID 的 70-71bit 为 01 (固定的保留值)

UUID v6

v6 是在 v1 的基础上改良的,官方文档明确说明「除非为了和 v1 兼容,否则应当使用 v7」

  1. 时间戳部分保留、重新排列以优化局部性

  2. 版本号使用 0110 即 v6

  3. 单调时钟部分(与保留位)不变

  4. 最后 48bit 的原 MAC 地址现在是 SHOULD 为伪随机数 + MAY 继续使用旧的 MAC 地址的行为

UUID v7

官方明确建议:如果可以的话,使用 v7 来替代 v1/v6(官方没有提及是否要替代 v4,但是我个人建议是替代,特别是在用作数据库主键的情况下应当使用 v7 来替代 v4 以提升局部性)

v7 的生成流程:

  1. 48 bit 的 Unix 毫秒级时间戳

    • 提示:与 v1/v6 采用的时间戳不同

  2. 为其余位生成并赋值随机数

  3. 设定 UUID 的 60-63bit 为 0111 (固定的版本号)

  4. 设定 UUID 的 70-71bit 为 01 (固定的保留值)

UUID v8

UUID v8 可以认为是一个提供「用户自定义实现」的规范,流程是

  1. 用户自行生成 128bit 随机位(实际被用到的是 122 bit)

    • 建议:这个生成是基于时间的

  2. 设定 UUID 的 60-63bit 为 1000 (固定的版本号)

  3. 设定 UUID 的 70-71bit 为 01 (固定的保留值)

V8 有以下特性:

  1. 不能对它的 uniqueness 作出假设(取决于实现)

  2. 这通常用于想要在 UUID 中附加一些自定义数据

  3. 文档明确说明,v8 不是 v4(122bit 全随机)的替代

UUID 的自定义编码

默认的 UUID 编码比较冗余,因此市面上诞生了很多的自定义编码

base62

base62 使用全部 62 个大小写字母+数字进行编码,可以说是最短的 UUID 编码方式,最终编码结果为 22 位

base58

base58 利用 58 个字符(123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz 数字与大小写字母,但不包含 0 O l I 四个容易引起混淆的字符)进行编码

base58 在事实上比 base62 更加流行,因为它尽管少了 4 个字母用于编码但最终编码出来的字符串同样是 22 位,却同时兼顾了人类可读性

base36

base36 利用全部数字和字母进行大小写不敏感编码

虽然我这里写了 base36,但事实上…… 我似乎从来没见人用它编码过 UUID😂

base32

base32 使用数字和字母进行大小写不敏感编码,并特殊处理了一些编码,最终编码结果为 25 或 26位(每5bit 使用一个字符编码,128bit 需要 25.6 个字符),并且存在几个不同的流派……

  • RFC4648

    • 使用 ABCDEFGHIJKLMNOPQRSTUVWXYZ234567 编码,= 做 pad

    • 一般提及的 base32 均是这个 RFC 所指定的

  • RFC4648 Hex

    • 使用 0123456789ABCDEFGHIJKLMNOPQRSTUV 编码,= 做 pad

    • 注:使用 hex 模式时应当始终使用术语 base32hex 而不要使用 base32

    • 优势:编码后按位比较时依然保持其排序顺序(base64 和 base32 缺乏)

  • Crockford

    • 解码时将 0 O o 三个视为相同,编码时使用 0

    • 解码时将 1 l L i I 五个视为相同,编码时使用 1

    • 不编解码 u U

    • 其他正常(大小写不敏感)

实现:Rust fast32

ulid

Spec

26 位编码,实质上就是利用 Crockford base32 实现,这里提出来主要是因为它太流行了

实现:Rust ulid, rusty_ulid

typeid

Spec

使用一个自定义前缀 + Crockford base32 编码实现