UUID 生成器

生成 v4 或按时间排序的 v7 UUID,也可解析你已有的 UUID。

5

随机 UUID

    解析一个 UUID

    粘贴一个,看看它是哪个版本,以及其中是否嵌入了什么。

    版本,以及其中要紧的那两个

    一共有八个版本,而对新项目来说,选择其实只在两个之间。其余几个值得认得出来,好在老系统里遇到时心里有数。

    版本由什么构成什么时候用它
    v4122 个随机位你需要不可猜测。令牌和标识符的默认选择。
    v7时间戳 + 随机数你需要一个数据库键。按创建时间排序。
    v1时间戳 + MAC 地址遗留方案。会泄露机器和时间。
    v3 / v5命名空间与名字的哈希你需要同样的输入永远给出同样的 ID。
    v6重排过的 v1迁移 v1 数据并希望它能排序。
    v8你自己决定的任何东西几乎永远用不上。

    索引问题,也就是 v7 真正的存在理由

    这是那个实用层面的论证,值得弄懂,而不是照单全收。

    数据库主键存放在一棵有序的 B 树里。插入连续的整数,每一行新数据都落在最右边缘:一个页面一直热在内存里,树整整齐齐地长大,写入很便宜。

    而插入 v4 UUID,每一行都落在随机的某处。整棵树上到处的页面都被碰到,每一个都得先读出来才能写回去,而页面写满了就要分裂。在任何有点规模的表上,写入吞吐会明显下降,索引也会碎片化——这个效应有充分的文档记载,UUID 主键的坏名声就是这么来的。

    v7 靠把时间戳放在最前面解决了这个问题。选上 v7 在上面生成一小把,看看它们共同的前缀:创建时间相近的值彼此挨着,于是插入就像整数那样追加在索引末尾,同时仍然保持全局唯一。你保住了 UUID 最初吸引人的那个性质——在任何地方生成一个 ID,不需要任何协调——而不必在每一次写入时为它付账。

    说得精确些:顺序在毫秒与毫秒之间是有保证的,在同一毫秒之内则没有。同一毫秒里生成的两个 v7 UUID,其顺序由随机尾部决定;RFC 9562 允许这样,也允许实现选择用一个计数器把它收紧。这对索引局部性的论证毫无影响,因为后者只取决于开头那段时间戳。

    两者都还剩下的那笔代价:16 字节对 4 或 8 字节,而且会被复制进每一个引用该键的索引里。在一张带好几个外键的大表上,那是实打实的磁盘。

    v7 会泄露什么

    任何持有该标识符的人都能读出那个时间戳。把一个 v7 UUID 粘进上面的检查器,它会告诉你它是在哪一毫秒创建的。

    通常无害。偶尔则不然:公开 URL 里的一个 v7 标识符会暴露一条记录是什么时候创建的,而这可能足以让人推断出注册速率、订单量,或者某份文档被倒填了日期。如果这要紧,那么最直截了当的划分是:凡是用户可见的用 v4,内部键用 v7。

    随机性必须是真的地方

    人人都引用的那套碰撞算术——2.7 × 10¹⁸ 个之后才有一半概率发生一次碰撞——假定的是 122 个货真价实的随机位。那是关于这个格式的陈述,不是关于你的实现的。

    基于 Math.random() 造出来的 UUID,既没有那个数字暗示的熵,也没有它暗示的不可预测性;而现实中发生的碰撞,基本上从来都是因为这个,而不是因为运气不好。两个在同一秒启动、种子相同的进程,会产出同一串序列。

    本页在 crypto.randomUUID() 存在时用它,否则用 crypto.getRandomValues。你自己的代码用的是什么,值得去核对一下,尤其当 UUID 被拿来当令牌用的时候——一个可预测的密码重置标识符,等于一次彻底的账号接管。

    怎样高效地存储它们

    36 个字符的文本形式很方便,也很浪费。以下几种替代方案,都是同样的 128 位:

    • 原生 UUID 类型——Postgres 有一个,它存 16 字节。就用它。
    • `BINARY(16)`——MySQL 的对应物,大约是 CHAR(36) 存储量的一半。
    • Base64——22 个字符,适合它必须是文本、而长度又要紧的场合。
    • Base32——26 个字符,不区分大小写;如果有朝一日会有人把它念出来,你要的就是它。

    相关

    对于需要人来手敲、而不是机器来存储的秘密,密码生成器用的是同一个随机源。想把 v7 UUID 里的时间戳读成别的格式,时间戳转换器会从那里接手。

    UUID 相关问题

    两个 UUID 会撞车吗?

    理论上会,实际上不会——前提是随机性是真的。一个 v4 UUID 有 122 个随机位,你大约要生成 2.7 × 10¹⁸ 个,才会遇到 50% 的概率发生一次碰撞——相当于每秒十亿个、连续生成 85 年。现实中出问题的从来不是数学,而是薄弱的随机源。基于 Math.random() 或者种子不佳的伪随机数生成器造出来的 UUID 是会碰撞的,也确实碰撞过,所以本页使用的是浏览器的加密级随机源。

    该用 UUID 做数据库主键吗?

    这取决于版本,而差别很大。v4 UUID 是随机的,于是连续插入会落在 B 树索引的随机位置上——页面分裂、索引碎片化,在大表上写入吞吐会有可测量的下降。v7 UUID 以毫秒时间戳开头,所以插入会像自增整数一样追加在末尾,同时仍然保持全局唯一。想用 UUID 做键,就用 v7。另有一项代价两者都逃不掉:16 字节对整数的 4 或 8 字节,而且每一个引用它的索引都要付这笔账。

    UUID v7 是什么?现在可以放心用了吗?

    它是按时间排序的:48 位 Unix 毫秒,后面跟 74 个随机位,于 2024 年 5 月在 RFC 9562 中标准化。它已是正式发布的标准,当前版本的 Postgres、多数语言生态和相当多的库都支持它。作为新项目的默认选择是合理的。唯一要留意的性质是它内嵌了创建时间,所以出现在 URL 里的 v7 标识符会大致泄露这条记录是什么时候产生的。

    UUID 足够安全,可以拿来当令牌吗?

    来自加密随机源的 v4 UUID 有 122 位熵,用作密码重置链接或会话标识符绰绰有余。但有两点要注意。第一,那个随机源必须真的是加密级的——crypto.randomUUID 是,多数库的实现是,而任何基于 Math.random() 的都不是。第二,其他版本并不适合:v1 内嵌时间戳,历史上还内嵌 MAC 地址;v3 和 v5 是对输入做的确定性哈希;v7 会泄露创建时间。如果它必须不可猜测,就用 v4。

    UUID 只有 16 字节,为什么写出来是 36 个字符?

    因为它是用十六进制写的——每字节两个字符,也就是 32 个——再加上四个连字符。有些系统改为存储那 16 个原始字节,MySQL 的 BINARY(16) 就是干这个的,能把存储量大致减半。另一些则用更短的文本编码:Base64 是 22 个字符,Base32 是 26 个且不区分大小写。它们全都是同样的 128 位,只是换了身衣服。

    最后审校于 。发现有内容过时了? 告诉我们.