UUID
UUID 是最经典的唯一 ID,号称「绝对不重复」。一个典型的 UUID 表示为 8246dba2-3737-4067-83f5-f2a8dd0fb2e8
介绍
UUID 事实上是一个 16 字节(128 位二进制)的编码,常见的文本格式只是一种 “base16“ 编码(利用 0-9
与 a-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 来生成
流程:
计算出
namespace
+name
(字节连接)的 MD5 Hash(16 字节 128 bit),直接赋值给 UUID规范里的 namespace 也是 UUID,计算 md5 时使用它的 16Bytes 表示形式
规范里的 name 可以使用任何文本字符串(需要可以转换为 bytes,其他文本编码的需要自行定义转换方式)
修改这个 UUID 的 60-63bit 为
0011
(v3 固定的版本号)修改这个 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 也很简单粗暴:随机生成
随机生成 16bytes(128bit),直接赋值给 UUID
修改这个 UUID 的 60-63bit 为
0100
(固定的版本号)修改这个 UUID 的 70-71bit 为
01
(固定的保留值)
UUID v6
v6 是在 v1 的基础上改良的,官方文档明确说明「除非为了和 v1 兼容,否则应当使用 v7」
时间戳部分保留、重新排列以优化局部性
版本号使用
0110
即 v6单调时钟部分(与保留位)不变
最后 48bit 的原 MAC 地址现在是 SHOULD 为伪随机数 + MAY 继续使用旧的 MAC 地址的行为
UUID v7
官方明确建议:如果可以的话,使用 v7 来替代 v1/v6(官方没有提及是否要替代 v4,但是我个人建议是替代,特别是在用作数据库主键的情况下应当使用 v7 来替代 v4 以提升局部性)
v7 的生成流程:
48 bit 的 Unix 毫秒级时间戳
提示:与 v1/v6 采用的时间戳不同
为其余位生成并赋值随机数
设定 UUID 的 60-63bit 为
0111
(固定的版本号)设定 UUID 的 70-71bit 为
01
(固定的保留值)
UUID v8
UUID v8 可以认为是一个提供「用户自定义实现」的规范,流程是
用户自行生成 128bit 随机位(实际被用到的是 122 bit)
建议:这个生成是基于时间的
设定 UUID 的 60-63bit 为
1000
(固定的版本号)设定 UUID 的 70-71bit 为
01
(固定的保留值)
V8 有以下特性:
不能对它的 uniqueness 作出假设(取决于实现)
这通常用于想要在 UUID 中附加一些自定义数据
文档明确说明,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 个字符),并且存在几个不同的流派……
使用
ABCDEFGHIJKLMNOPQRSTUVWXYZ234567
编码,=
做 pad一般提及的 base32 均是这个 RFC 所指定的
使用
0123456789ABCDEFGHIJKLMNOPQRSTUV
编码,=
做 pad注:使用 hex 模式时应当始终使用术语
base32hex
而不要使用base32
优势:编码后按位比较时依然保持其排序顺序(base64 和 base32 缺乏)
解码时将
0
O
o
三个视为相同,编码时使用0
解码时将
1
l
L
i
I
五个视为相同,编码时使用1
不编解码
u
U
其他正常(大小写不敏感)
实现:Rust fast32
ulid
26 位编码,实质上就是利用 Crockford base32 实现,这里提出来主要是因为它太流行了
实现:Rust ulid, rusty_ulid
typeid
使用一个自定义前缀 + Crockford base32 编码实现