← 返回博客首页

ULID 与 UUID 之争:时间排序标识符实战图解

一个分布式主键的经典提问

给消息表或订单表选主键时,你大概遇到过这个两难:

UUID v4 : 550e8400-e29b-41d4-a716-446655440000   (随机,36 字符)
ULID    : 01HZ3G7DZP5XA5Z4G5TQXW2Q7T            (可排序,26 字符)

两者都全局唯一、都能离线生成,但一次排序测试就能看出分野:把按 UUID 排的 100 行按创建时间打乱,你会看到字典序和真实顺序毫无关联;换 ULID,字典序恰等于创建顺序。

这一条差异,决定了它们在数据库索引、日志聚合和可排查性上的不同表现。

ULID 的构造:把时间编码进前缀

ULID 只有 48 bit 时间戳 + 80 bit 随机数:

 48 bit     80 bit
 timestamp  randomness
 ────────── ───────────────────────────── ──
 01HZ3G7DZP              5XA5Z4G5TQXW2Q7T
  • 前 48 位是自 Unix 纪元起的毫秒数,编码成 10 个 Base32 字符;
  • 后 80 位是加密随机数,编码成 16 个字符;
  • 总长 26 字符,字符集为排除 I/L/O/U 的 Crockford Base32。

解码时间戳很简单:取出前 10 字符按 Base32 反解即得毫秒时间。这也是「可排序」的根基——先比时间,时间相同才比随机。

三种主流标识符对比

特性 UUID v4 ULID Snowflake(类)
长度 36 字符 26 字符 19 位十进制数
时间可排序 是(毫秒级) 是(秒级+序列号)
离线可生成 完全离线 完全离线 需节点 ID / 协调
跨节点强排序 否(受时钟漂移影响) 依实现
可读性 中(连字符分组) 高(紧凑无连字符) 高(纯数字)

Snowflake 系为了压到更短或对齐单一数据中心,常牺牲离线性(需要 worker ID);ULID 在多语言里都能纯函数离线生成,部署最简单。

单机/单节点如何保证「同一毫秒内递增」

很多实现提供 monotonic 模式:若同一毫秒内再次生成,就把随机位视作计数器递增,直到溢出或进入下一毫秒。对数据库顺序插入这点很关键——同一毫秒内乱序也会触发页分裂。

// JavaScript 示意:同一毫秒内递增随机位
let lastGen = null, lastRand = 0n
function ulid() {
  const now = Date.now()
  if (now === lastGen) lastRand++          // 单调递增
  else { lastGen = now; lastRand = 0n }
  return encode(now, lastRand)
}

为什么顺序主键对数据库更友好

InnoDB 这类聚簇索引表把主键当作物理排序依据。随机主键会让一行新数据插入到页面中部,触发页面分裂、回表和随机写放大;顺序主键则总是追加到页面尾部。数据量千万级时,两者在写入吞吐和缓存命中率上的差距会非常明显。

这也是每当你打开自己的工具站、用 ULID 生成器生成主键时的常见出发点:如果你要的是「全局唯一 + 大致时序」,ULID 常是比 UUID v4 更贴合数据库行为的默认选择。

隐私权衡:优点与代价要一起看清

编码进 ULID 的时间戳意味着:任何拿到 ID 的人都知道生成时刻。这既是日志排障的救命线索,也可能是泄露给用户的弱点。建议:

  • 面向用户公开的标识符(订单号、邀请码)若需隐藏时间,用 UUID v4;
  • 服务器内部、数据库主键、日志/事件 ID,优先 ULID;
  • 合规敏感的创建时间,考虑把时间字段单独加密存储,而不用可排序 ID 间接外泄。

选型速查

选 ULID,当:关系库主键想要顺序写入、日志需按时间聚合、想离线简单生成且长度要短。

选 UUID v4,当:暴露给终端用户、严格不希望泄露时间、需要与外部系统按标准互操作。

选 Snowflake/数据库序列,当:跨节点硬性需要严格全局顺序、已有多数据中心且能提供 worker ID。

动手验证

拿一个你手头的 ULID 生成器或脚本,生成一批后按字典序排序,再对照内部实现的编码——你会发现它们天然按创建顺序排好。把同一个需求换成 UUID v4 重跑一次,排序就打乱了。这个小实验能让你把前面讲的特性直观留在记忆里。


常见问题

ULID 和 UUID v4 的核心区别是什么?

最核心的差异是**时间可排序性**:ULID 把毫秒级时间戳编码进前 48 位,因此按字典序就能得到创建顺序;UUID v4 完全随机,创建先后与字典序无关。此外 ULID 只有 26 字符(Base32 Crockford 编码),比 UUID v4 的 36 字符更紧凑。代价是 ULID 可被部分推测出创建时间(隐私权衡),且若把 ULID 泄露出创建时间会带来一定信息暴露。

为什么分布式系统喜欢 ULID 而非 UUID?

关系型数据库对**顺序主键**的索引更友好:B+ 树按序插入可减少页分裂,回表和随机 IO 都更低。ULID 在普通单机也可用单调前缀设施(monotonicity)保证同一毫秒内递增,避免插入无序导致的性能退化。而 UUID v4 随机主键在超大表上常引发索引碎片与缓存命中率下降。ULID 同时保留了全局唯一性和近似时序,尤其适合日志、事件溯源和消息队列这种『天然按时间聚合』的场景。

ULID 会泄露信息吗?

会,这是有意为之的权衡。ULID 前 48 位的时间戳可被解码成毫秒级的创建时间,任何拿到 ULID 的人都能推断出『这个 ID 在何时生成』。若用例是公开暴露给终端用户的标识符(如订单号、优惠码),这可能是不希望暴露的信息;若用于服务器内部日志或数据库主键,这种可溯源性反而是优点。Crockford Base32 编码刻意剔除了易混淆字符(I/L/O/U),降低了人工复制出错率。

ULID 能在没有统一时钟的分布式环境保证排序吗?

不能严格保证。ULID 依赖各节点的本地时钟;跨时区或时钟漂移的节点会打破全局排序。常见缓解:同一节点内启用单调递增保证(monotonic),并对跨节点用『有界时间差』约定(如 NTP 校准 + 容忍窗口)。若业务硬性要求**跨节点全局严格排序**,应改用带分布式序的方案(如 Snowflake 系、数据库序列、或引入协调服务),而不是纯 ULID。

← 返回博客首页