1 前言
在数据库规划和管理中,有一个比较传统且简便的主键(Primary Key)做法,即使用数字自增字段作为主键。在早期,几乎全部都是这么用的。现在,如果是简单的系统,也仍然有大量系统采用这种方式。直到一些分布式系统的出现,才对主键有了全局唯一性的要求,出现了一些使用 UUID 作为主键的设计。
自增数字字段作为主键,确实非常方便,无需维护,并且保留自然的顺序,可排序、可检索,还非常直观。但是它也存在一个让人非常难受的弊端,就是它是操作完成后才知道具体数值的。
也就是说,要新增一条数据,并不知道这条数据的 ID 是多少,直到把数据插入数据库、完成操作之后,才能获取它的 ID。
在 CRUD 中,除了 C 之外其他操作都是很自然的:先有数据,然后执行操作。只有新增(Create)比较特殊,是先准备数据,执行操作,再获取操作结果,得到 ID。
这种模式在单一数据库内部通常没有太大问题,但对于现代应用来说,ID 往往不只是数据库内部字段,而是会参与 API、消息、文件、缓存、日志以及跨系统业务流程。此时,必须先完成数据库写入才能获得 ID,就会对整个流程产生约束。
这种后知后觉的操作设计,不仅使用起来不方便,还会带来流程上的问题。
例如,很多业务是跨系统的,通过 API 调用或者消息传递完成。如果业务对象必须先落库才能获得 ID,那么 ID 的产生就成为整个业务流程中的一个阻塞点。
另外一个问题是批量处理。
假设有一批通讯录或者材料清单,几百到几万条记录。一次性导入数据库之后,原始数据又进行了更新,可能删除了过期数据,也可能修正了一批错误。
如果使用数字自增 ID,那么原始数据和数据库中的记录之间就缺少一个可以提前确定的稳定标识。特别是在需要反复导入、同步或者跨系统传递的场景下,处理起来会比较麻烦。
2 UUID
为了解决上面的问题,就需要引入额外的、固定的、唯一的主键。对于一个通用方案来说,UUID 是最简单的选择之一。
UUID 最大的优势,就是可以事先生成 ID,而不是在数据库写入之后才能获得 ID。
这样,UUID 可以在数据进入数据库之前就作为整个业务对象的身份标识,并贯穿 API、消息、文件以及数据库等多个环节。
对于批量管理来说也特别简单:事先生成的 UUID 在整个流程中都可以作为主键来建立关联关系。
UUID 用作主键之后,CRUD 的流程也变得一致,不再因为等待数据库生成 ID 而卡住流程。
但是 UUID 也不是万金油。
传统 UUID 看起来类似:
b9c83eaa-7819-11f1-af19-7cd30b70e7
对于机器而言,这种表示方式非常适合唯一标识;但对于人来说,UUID 基本只是一串难以理解的字符。
相对于数字自增 ID:
99、3、12345
我们可以非常直观地知道它们的数值大小、顺序以及大致位置。
数字 ID 不仅可读、可理解,还可以顺口告诉其他人,用于人工沟通、排查问题和运维。
传统 UUID 则基本没有这些信息。
这也是 UUID 在实际数据管理中经常遇到的问题:
- 不容易人工阅读;
- 不容易判断先后关系;
- 不容易判断大致生成时间;
- 日志和运维排查时不够直观;
- 人工沟通时很难准确描述。
当然,也可以采用两种机制:数字自增字段作为主要主键,UUID 作为辅助唯一键。
这种方式可以同时获得两种机制各自的优点,但同时也意味着系统中存在两套身份标识。必须明确两个 ID 各自的应用场景,并进行严格约束,避免混淆。
因此,一个更理想的方向是:
能否让一个 ID 同时具备 UUID 的全局唯一和可预先生成能力,又具备数字 ID 的时序、可读和可理解特性?
3 时序标识符
三恒移俗在实践中总结出一种基于 UUID 的时序标识符规范,简称 SeqUUID(Sequential UUID)。
其核心思想非常简单:
将 UUID 中的一部分空间用于承载时序信息,其余部分继续使用 UUID 的唯一性机制。
基本机制如下:
生成一个全局唯一的 UUID。
例如:
b9c83eaa-7819-11f1-af19-7cd30b70e7生成一个包含时序信息的前缀。
最简单的方式,可以直接使用时间戳,例如:
201808042050使用时序信息替换 UUID 的前半部分,形成 SeqUUID:
20180804205039ee1719b1662b6b70e7
这样得到的标识符,同时具有两部分属性:
- 前部:提供时序信息;
- 后部:提供随机性和全局唯一性。
因此,SeqUUID 可以同时获得 UUID 和数字 ID 的部分优点:
- 可以预先生成;
- 不依赖数据库自增;
- 可以跨系统传递;
- 可以用于分布式系统;
- 可以进行时序排序;
- 人可以直接阅读其中的时间信息;
- 可以根据 ID 大致判断生成时间;
- 数据库中可以按照 ID 进行索引和排序。
需要特别说明:
SeqUUID 的“时序”并不等同于全局严格递增。
在单机、低并发以及足够时间精度的情况下,ID 的排序通常可以很好地反映生成先后。
但在分布式环境中,不同节点可能存在时钟偏差;当多个节点在相同时间精度内高并发生成 ID 时,仅依靠时间前缀无法保证全局生成顺序。
因此,SeqUUID 的规范语义应当理解为:
提供时序排序能力,而不是提供全局严格单调递增序列。
如果业务确实要求严格的单调顺序,则需要额外引入节点序列、计数器或者中心化序列机制。
4 与 UUID 标准的关系
SeqUUID 并不是对 UUID 标准版本号的重新定义,而是一种基于 UUID 思想的标识符规范。
理解 SeqUUID,首先需要了解 UUID 标准本身的发展。
4.1 RFC 4122
RFC 4122 是长期以来最常用的 UUID 标准,于 2005 年发布。
它定义了 UUID 的基本格式以及多个 UUID 版本,包括:
- UUIDv1:基于时间和节点信息;
- UUIDv3:基于 MD5 的名称型 UUID;
- UUIDv4:基于随机数;
- UUIDv5:基于 SHA-1 的名称型 UUID。
其中最常见的是 UUIDv4。
UUID 本身固定为 128 bit,即 16 字节,不需要中心化的 ID 分配机构,因此非常适合分布式环境中预先生成。
4.2 RFC 9562
2024 年发布的 RFC 9562 对 UUID 规范进行了重新整理,并正式取代 RFC 4122。
RFC 9562 保留了 UUID 的 128 bit 基础结构,同时新增了 UUIDv6、UUIDv7 和 UUIDv8 等定义。
其中与时序标识符最相关的是 UUIDv7。
4.3 UUIDv7
UUIDv7 专门针对时间排序场景设计。
它在最高有效的 48 bit 中存储 Unix 时间戳,单位为毫秒;其余空间主要用于随机数据,也允许实现使用子毫秒时间、计数器等方式进一步增强同一毫秒内的单调性。
其结构可以简化理解为:
┌──────────────────────────────┐
│ Unix 时间戳(毫秒) 48 bit │
├──────┬───────────────────────┤
│ v7 │ 随机/计数等信息 │
└──────┴───────────────────────┘
因此 UUIDv7 已经解决了传统 UUIDv4 最大的一个问题:
UUID 本身可以携带时序信息,并且可以按照时间顺序进行排序。
RFC 9562 也明确建议,在可能的情况下优先使用 UUIDv7,而不是 UUIDv1 或 UUIDv6。
5 SeqUUID 与 UUIDv7 的区别
SeqUUID 与 UUIDv7 的目标存在明显重叠。
两者都试图解决传统 UUID 缺少时序信息的问题。
主要区别在于:
| 特性 | UUIDv4 | UUIDv7 | SeqUUID |
|---|---|---|---|
| 标准 | RFC 4122 / RFC 9562 | RFC 9562 | 本规范 |
| 长度 | 128 bit | 128 bit | 取决于实现 |
| 可预先生成 | 是 | 是 | 是 |
| 全局唯一性 | 是 | 是 | 是 |
| 时间信息 | 无 | 有 | 有 |
| 时序排序 | 否 | 是 | 是 |
| 人类可读时间 | 否 | 否 | 是 |
| 人类可理解性 | 低 | 中 | 高 |
| 适合数据库索引 | 一般 | 好 | 好 |
| 分布式严格顺序 | 否 | 不保证 | 不保证 |
| 与 UUID 标准兼容 | 是 | 是 | UUID 派生格式 |
UUIDv7 的优势在于标准化程度高、长度固定、结构紧凑,并且专门针对时序排序进行了设计。
SeqUUID 的主要优势则不是重新发明一种更强的 UUID,而是:
把时序信息直接以人可以阅读和理解的形式放进标识符。
例如:
UUIDv7:
0199c123-4567-7abc-8def-123456789abc
人可以判断它是 UUIDv7,却很难直接从字符串中读出具体时间。
SeqUUID:
20261008223039ee1719b1662b6b70e7
则可以直接看到:
2026-10-08 22:30
在数据库排查、日志分析、接口调试、运维以及人工沟通等场景中,这种差异非常明显。
因此,两者并不是简单的替代关系。
可以将它们理解为:
UUIDv7 更偏向机器友好和标准化的时序 UUID;SeqUUID 更偏向人类可读、可理解的时序标识符。
6 时序标识符的设计原则
SeqUUID 的核心并不是规定某一种固定的时间格式,而是规定:
标识符应当在保持全局唯一性的同时,在高位保留具有稳定排序意义的时序信息。
因此可以根据具体场景选择不同的时序精度。
例如:
202610082230
可以精确到分钟。
也可以使用:
20261008223039
精确到秒。
还可以使用更高精度的时间戳。
时间精度越高,同一时间窗口内发生冲突的概率越低,但人类可读性也会随之下降。
因此应该根据业务需求 1进行选择,而不是单纯追求更高精度。
对于需要更强顺序能力的场景,也可以在时序部分增加本地序列号、节点信息或者计数器。
但需要注意:
增加这些机制之后,SeqUUID 将从单纯的时序标识符逐渐演变成带有分布式序列分配机制的 ID 系统。
这属于更高一级的需求,不应成为 SeqUUID 基础规范的必要条件。
7 适用场景
时序标识符可以用于大量原本使用数字自增 ID 或 UUID 的场合:
- 数据库主键;
- 数据库业务唯一 ID;
- API 对象 ID;
- 消息 ID;
- 事件 ID;
- 日志关联 ID;
- 文件或对象 ID;
- 分布式系统中的业务对象 ID;
- 批量导入和同步数据的稳定 ID。
尤其适合以下场景:
需要提前生成 ID
数据对象在进入数据库之前就需要拥有稳定身份。
需要跨系统传递 ID
ID 可以在数据库、API、消息系统和文件系统之间保持一致。
需要人工排查
运维人员可以直接从 ID 中看到时序信息,而不需要额外查询数据库。
需要按照时间大致排序
ID 本身即可提供基本的时序排序能力。
希望避免数字自增 ID 的中心化分配
不需要依赖数据库生成下一个 ID。
8 局限性
时序标识符并不能替代所有类型的 ID。
首先,它通常是字符串,而传统数字自增 ID 是整数。这对于旧系统的直接改造可能存在兼容问题,需要升级代码、数据库字段以及相关接口。
其次,时序信息依赖于系统时钟。
如果机器之间存在时钟偏差,或者系统发生时钟回拨,那么 ID 的排序不一定严格反映真实生成顺序。
再次,在高并发、分布式环境中,同一时间窗口内可能同时生成大量 ID。
因此:
时序排序 ≠ 全局严格递增。
如果业务只需要“基本按照时间排序”,SeqUUID 已经足够。
如果业务要求:
A 一定小于 B,且 A 一定先于 B 被生成
那么就需要额外使用序列号、分布式协调机制或者中心化 ID 分配服务。
这也是 UUIDv7 同样面对的问题。RFC 9562 允许 UUIDv7 使用子毫秒时间和计数器增强局部单调性,但这并不意味着在任意分布式环境中都能获得全局严格顺序。
9 与数字自增 ID、UUIDv4、UUIDv7 的定位
可以简单总结为:
数字自增 ID
↓
强顺序
强可读
但无法提前知道
并且依赖中心化分配
UUIDv4
↓
可提前生成
分布式友好
全局唯一
但基本不可读、不可理解、无时序信息
UUIDv7
↓
可提前生成
分布式友好
全局唯一
具备时序排序
但时间信息对人不直观
SeqUUID
↓
可提前生成
分布式友好
全局唯一
具备时序排序
并且直接携带人类可理解的时间信息
因此,SeqUUID 的价值并不是证明 UUIDv7 不够好。
相反,UUIDv7 已经从标准层面解决了 UUID 的时序问题。
SeqUUID 所关注的是另一个经常被忽略的问题:
ID 不只是给机器比较的,也是给人看的。
在实际的软件工程中,数据库查询、日志分析、故障排查、接口调试以及人与人之间的沟通,都需要阅读 ID。
一个能够直接表达时间信息的 ID,在这些场景下具有明显的工程便利性。
10 总结
数字自增 ID 解决了顺序问题,但无法提前生成,并且依赖中心化分配。
传统 UUID 解决了全局唯一和提前生成的问题,但牺牲了可读性和时序信息。
UUIDv7 则进一步把时间排序能力正式纳入 UUID 标准,是现代 UUID 中非常适合数据库和分布式系统的一种方案。
SeqUUID 在此基础上关注的是:
让时序信息不仅能够被机器排序,也能够被人直接阅读和理解。
因此,时序标识符的核心目标可以概括为:
全局唯一、可预先生成、时序可排序、时间可理解。
它不试图提供全局严格递增序列,也不试图取代 UUIDv7,而是提供一种更加偏向人类可读性的时序标识符设计方式。
11 参考
- RFC 4122:A Universally Unique IDentifier (UUID) URN Namespace
- RFC 9562:Universally Unique IDentifiers (UUIDs)
- UUIDv7:RFC 9562 Section 5.7
软件工程中的业务需求. https://srs.pub/theory/business-req.html↩︎