在当今高并发、高可用的互联网系统中,唯一标识符(Unique Identifier)是数据架构里最基础却又最常被忽视的一环。我们几乎每天都在用 UUID,但你真的了解它的短板吗?UUID 虽然通用、稳定、跨平台,但在高频写入、需要排序、或对数据库性能极度敏感的场景下,它其实是个“甜蜜的负担”。
今天要介绍的 ULID(Universally Unique Lexicographically Sortable Identifier),正是为解决这些问题而生的未来级 ID 方案。它不仅兼容 UUID,还能让 PostgreSQL 这类数据库的写入性能和索引效率直接起飞。更关键的是——它完全无需重构你的数据库表结构!
UUID 的四大“坑”,你踩过几个?
UUID(通用唯一识别码)自诞生以来,几乎成了分布式系统生成唯一 ID 的默认答案。
但我们必须正视它的几个本质缺陷:
第一,UUID v1/v2 依赖设备 MAC 地址,在现代云环境、容器化部署中根本不可行;
第二,UUID v3/v5 需要一个“唯一种子”,这在无状态服务中很难保证;
第三,现在最常用的 UUID v4 全靠随机数,虽然避免了隐私问题,却导致数据库索引严重碎片化——想象一下,每次插入新记录,B+树索引都要在随机位置分裂节点,这不仅拖慢写入速度,还大幅增加磁盘 I/O;
第四,UUID 太长(36位字符),且包含连字符和大小写混用,对人类极不友好,也不利于 URL 传递。
这些问题在中小型项目中可能还能忍,但在日活百万、写入量上亿的系统里,它们就是性能瓶颈的根源。
ULID:128 位的“时间+随机”精妙结构
ULID 的设计哲学非常清晰:保留 UUID 的 128 位长度和兼容性,但重新分配这 128 位的用途。它把前 48 位用于 Unix 时间戳(毫秒级精度),后 80 位用于加密安全的随机数。
这意味着:任何 ULID 都自带“出生时间戳”,天然具备时间顺序。更重要的是,因为时间戳在高位,ULID 在字典序(lexicographic order)下是可排序的!你不需要额外字段记录创建时间,只要按 ID 排序,就能得到插入顺序。
而且,ULID 使用 Crockford’s Base32 编码(排除 I、L、O、U 字母以防混淆),输出为 26 位纯大写字符串,无连字符、无特殊符号、URL 安全、肉眼友好。举个例子:01KANDQMV608PBSMF7TM9T1WR4 ——是不是比 UUID 好读太多?
为什么 ULID 能直接写入 PostgreSQL 的 UUID 列?
这可能是最让人惊喜的一点:你完全不需要改动数据库 schema!PostgreSQL 的 UUID 类型本质上是一个 128 位的二进制值。而 ULID 虽然通常以字符串形式展示,但其底层也是 128 位数据(48+80)。
在 Go 语言生态中,oklog/ulid 包实现了 database/sql/driver.Valuer 和 encoding.TextMarshaler 接口,这意味着当 pgx 驱动(PostgreSQL 的高性能 Go 驱动)遇到 ULID 类型时,会自动将其转换为符合 UUID 二进制格式的字节流。
因此,你可以直接把 ULID 插入到 UUID 类型的列中,PostgreSQL 不会报错,还能完美保留排序特性。这种“零侵入式升级”让 ULID 成为现有 UUID 系统平滑迁移的理想选择。
实战演示:用 Go + pgx + ULID 一键生成可排序主键
下面这段代码展示了如何用 Go 语言轻松集成 ULID 到 PostgreSQL 项目中。我们创建一张名为 ulid_test 的表,主键为 UUID 类型,然后分别插入 UUID v4 和 ULID 类型的记录。注意看,ULID 的插入完全不需要格式化字符串,直接传入 ulid.ULID 对象即可,驱动会自动处理底层转换。
package main |
运行后你会看到类似这样的输出:
Inserted ULID: 019aaae4-be9c-d307-238f-be1692b3e8d7 |
查询结果震撼:ULID 自动按插入顺序排列!
执行这条 SQL:
select * from ulid_test where kind = 'ulid' order by id; |
你会发现返回的记录顺序和你插入的顺序完全一致!而如果你用 UUID v4 做同样操作,顺序则是完全随机的。这意味着:在日志系统、消息队列、用户行为追踪等需要“天然时间序”的场景中,ULID 不仅省去了 created_at 字段,还避免了额外的索引开销。数据库的 B+树索引会把新 ULID 持续追加到末尾,极大减少页分裂,显著提升写入吞吐量。对于 OLTP 系统来说,这简直是性能优化的“外挂”。
ULID 的极限能力:每毫秒 1.21e+24 个 ID,够你用到宇宙尽头
你可能会担心:如果系统在一毫秒内生成太多 ULID,会不会冲突?ULID 的 80 位随机空间意味着每毫秒可生成 2^80 ≈ 1.21 × 10^24 个唯一 ID。
这是什么概念?就算你每毫秒生成 10 亿个 ID,也需要超过 3800 万年才会用完!而且 ULID 实现了“单调递增模式”(monotonic mode):在同一毫秒内,如果多次调用生成器,它会自动将随机部分 +1,确保字典序严格递增。
这种设计既保证了唯一性,又避免了同一毫秒内 ID 乱序的问题。当然,如果你真能在一毫秒内生成超过 2^80 个 ID……那你可能已经统治了银河系,这个问题也不重要了。
警惕“热点写入”:ULID 的唯一软肋
凡事皆有两面。ULID 的时间有序性在带来索引优势的同时,也可能引发“热点写入”(hotspot writes)问题。
因为所有新记录都集中在索引的末尾,数据库的写入压力会集中在最新的数据页上。在极端高并发场景下(比如每秒数十万写入),这可能导致该数据页成为瓶颈,甚至引发锁竞争。不过,这种问题通常只出现在超大规模系统中(如 Twitter、支付宝级别),对于绝大多数应用,ULID 带来的性能提升远大于这点潜在风险。如果真遇到热点问题,也可以通过分片、写缓冲、或改用 UUID v7(带伪随机扰动)等方式缓解。
ULID 与 UUID v7 的殊途同归
有趣的是,ULID 的成功已经影响了官方标准。正在推进的 UUID v7 提案,其核心设计几乎就是 ULID 的翻版:时间戳 + 随机数,字典序可排序,兼容 UUID 格式。
这说明行业已经达成共识:传统 UUID v4 的时代正在过去,时间有序的 ID 才是未来。而 ULID 作为先行者,早已在 GitHub、GitLab、Stripe 等知名项目中落地实践。