什么是 ULID
ULID 是一种新型的唯一标识生成方案,全称为 “Universally Unique Lexicographically Sortable Identifier”。其设计目标是在保证全局唯一性的同时,具备“字典排序”(lexicographically sortable)特性。
从结构来看,ULID 通常是一个 128 位(16 字节)长度的标识,它将前面的一部分用于时间戳(以毫秒为单位),剩余用于随机数(80 位随机)——这样就既编码了创建时间,也保留了随机熵来源。规范中指出:48 位用于 UNIX 毫秒时间戳,80 位用于随机数。
在字符串表示上,ULID 常被编码为 26 个字符(使用 Crockford Base32 编码:例如 01ARZ3NDEKTSV4RRFFQ69G5FAV)
因此,ULID 在设计上具有以下几个显著特点:
-
可排序性:因其前缀是时间戳,相近生成的 ULID 会按生成顺序在字符上也大致“增大”。
-
唯一性与分布式生成:任意节点都可以生成 ULID,无需集中协调,同时由于随机熵较大,冲突概率极低。
-
紧凑且 URL-安全:26 字符、无特殊字符、基于 Base32,更适用于人阅读、日志、URL、数据库键等。
例如,某次生成两个 ULID:
01BX5ZZKBKACTAV9WEVGEMMVS0
01BX5ZZKBKACTAV9WEVGEMMVS1
可以看到,第二个相比第一个在同一毫秒生成的情况下,其随机部分被规范化地“+1”以维持排序性。
ULID 与 UUID 的主要区别
下面从多个维度分析 ULID 与传统 UUID(常用版本如 UUID v4)之间的差别。
1. 结构与编码
-
UUID(如版本4)为 128 位随机或部分时间基、混合其它元素,其文本形式典型为 36 字符(包括 4 个连字符),例如
123e4567-e89b-12d3-a456-426655440000。 -
ULID 同样为 128 位,但时间戳+随机数组合(48 位时间戳 + 80 位随机数)为典型结构。编码为 26 字符 Base32 字符串,无连字符。
2. 排序 / 索引特性
-
UUID(v4)几乎完全随机,无法根据文本值判断生成时间或排序先后,这对于数据库索引容易造成随机插入、索引碎片化等问题。
-
ULID 因时间戳前缀,生成的标识在字典/字符排序上大致反映生成顺序:早生成 < 晚生成(当在不同毫秒)——这有利于数据库索引聚集、顺序插入、分页查询(cursor pagination)等场景。
3. 可读性与紧凑性
-
UUID 的长度和形式(36 字符包括连字符)在某些场景不够紧凑。
-
ULID 的 26 字符 Base32 编码,更简洁、无特殊字符、URL 更友好,复制、追踪、日志体验更佳。
4. 时间信息暴露与安全性考量
-
UUID(v4)基本不包含生成时间或节点信息(完全随机),因此隐蔽性更强。
-
ULID 显式编码时间戳(毫秒级)在前 48 位,因此从 ULID 本身可解析出生成时间。这在某些业务中可能暴露生成时机,需考虑隐私/安全风险。
5. 支持度及标准化成熟度
-
UUID 规范(如 RFC 4122)已有多年历史,广泛支持几乎所有语言、数据库。
-
ULID 虽受到越来越多关注,但生态/库支持相比 UUID 略逊;规范也没有像 UUID 那样被广泛标准化。
6. 使用适用场景
-
若只需唯一标识、排序不重要、或者系统对标准支持强依赖,UUID 是较安全、传统的选择。
-
若需要标识带时间顺序、数据库索引性能优化、使用 Base32 紧凑字符串、生成顺序有意义,则 ULID 是一个优选替代。
ULID 的最佳实践
下面是一些在真实系统中使用 ULID 时值得注意、推荐的做法,以帮助你规避常见坑、发挥其优点。
1. 在合适的场景使用
-
如果你的系统有大量写入、新记录按时间插入、希望数据库主键按时间顺序聚集(减少 B-tree 索引碎片),那么使用 ULID 很合适。
-
如果只是简单的小系统、或者生成标识主要用于外部暴露、且不想透露生成时间,则也可以继续使用 UUID。
-
在需要极强安全性(例如生成密钥/令牌),因 ULID 时间可被解码,可能不是最佳选择。
2. 生成实现注意
-
选用成熟可靠的 ULID 库,确保其随机熵来源量足、同一毫秒内增量逻辑正确(“单线程或单进程”生成时的“monotonic”模式)。
-
在分布式环境、多进程/多节点同时生成的情况下,不能保证全局严格的“单调 ++”行为,仅能保障在单个生成器内。规范亦提示“如果跨机器,单机 monotonic 不起作用”。
-
若使用 monotonic 模式(同一毫秒内随机部分递增),需确认不会在极端高并发下导致溢出或冲突。规范指出:如果同一毫秒内生成超过 2^80 次,会溢出。
3. 存储与索引策略
-
将 ULID 用作数据库主键(如 VARCHAR(26) 或 CHAR(26))时,建议同时考虑字符编码排序与数据库构建索引行为。
-
对于关系型数据库(比如 PostgreSQL、MySQL),若主键由 ULID 串表示,排序和分区都可基于其值进行。许多系统利用 ULID 的时间前缀做范围查询、分页、归档管理。
-
若数据库支持二进制存储(16 字节)也可将 ULID 按二进制存储,从而节省空间且提升性能。检查所使用库是否支持将 Base32 表示转为二进制。
4. 外部暴露 &安全考虑
-
如将 ULID 暴露在 URL、外部接口、日志中,要注意其时间戳暴露可能泄露“生成时间”这一信息。若业务对时间隐私敏感,可能需规避或加掩码。
-
切勿将 ULID 视为“密钥”或“安全令牌”。因为虽然随机部分为 80 位,但时间戳 +可能的递增逻辑会降低其不可预测性。若需要真正保密的标识,应使用专门的安全令牌。
5. 与 UUID 的兼容与迁移
-
若系统当前使用 UUID,但希望切换为 ULID,可考虑如下步骤:
-
新表或新业务使用 ULID,旧业务继续使用 UUID。
-
数据库主键类型如 UUIDBINARY 或 CHAR(36) 可改为 CHAR(26) / BINARY(16) 存 ULID。注意迁移索引结构。
-
若系统中已有 UUID 且暴露给外部,不便彻底更换,也可在外部暴露界面使用 ULID,而内部仍存 UUID 或自增 ID。
-
-
确保所有语言/库对 ULID 支持情况成熟,避免生成方式不一致导致排序/唯一性偏差。
6. 监控与运营
-
在高并发生成场景下,建议监控生成速率、检测是否多节点可能在同一毫秒冲突。虽然冲突概率极低,但在极端场景仍需警惕。
-
若日志或监控系统大量使用 ULID,利用其时间戳前缀可以快速过滤 “自某时间之后生成的 ID”。这是一个实用优势。
-
对于长期存储、归档的系统,ULID 自带时间戳有助于分区、归档逻辑:如可按生成时间批次将数据打包。
四、小结
ULID 是一种现代化、面向排序与性能优化的唯一标识方案。相比传统 UUID,其优势在于:可排序、紧凑、URL 友好,更适合高写入、按时间查询、分页、索引优化等场景。但并非在所有场景都替代 UUID,特别是在安全、隐私、或标准兼容性要求极高的系统中,UUID 仍是可靠选择。
使用 ULID 时应结合业务场景、数据库架构、生成策略、外部暴露需求进行权衡,并遵循上文所述的最佳实践,以充分发挥其价值而避免潜在风险。