常见版本
- v1:基于时间戳 + MAC,可排序但可能泄露机器信息;
- v4:纯随机,最常用,碰撞概率极低;
- v7:带时间前缀、可按插入排序,新项目推荐。
作主键的取舍
| 优点 | 缺点 |
|---|---|
| 分布式可独立生成 | 比自增整数长,索引更大 |
| 不暴露总量 / 顺序 | 无序写入让 B+ 树索引碎裂 |
两个提醒
- 不是密码学秘密:UUID 可枚举,别用它做重置令牌或权限依据;
- 需要排序用 v7:v4 无序,列表按时间排要另存时间戳。
动手试试
生成标识符:UUID 生成。
实战案例:三个标识符设计失误
- 用自增 ID 做对外标识:
/order/1024直接暴露业务量,还能被遍历抓取。对外改用 UUID / ULID,内部仍保留自增主键。 - 把 NanoID 当密码用:NanoID 是随机标识符而非密钥,默认字符集与长度不足以承载秘密;令牌应使用足够熵的密码学随机值并设置过期。
- 忽略大小写与排序:入库时转成小写,查询却用大小写混写导致查不到;若需要按时间排序,应选 ULID 而不是 v4。
常见问题(FAQ)
UUID v4 和 ULID 怎么选?需要按生成时间排序、希望更短可读时选 ULID;纯随机且无排序需求选 v4。UUID 会泄漏信息吗?v4 不会;v1 编码了 MAC 地址与时间戳,可能泄漏机器信息。能直接做数据库主键吗?可以;但完全随机的 v4 会造成索引随机写,写入密集场景更宜用有序的 ULID。重复概率真的可以忽略吗?122 位随机下碰撞概率极低,但分布式唯一性仍建议加唯一索引兜底。
生成与存储的实操要点
- 生成源:必须用密码学安全随机数(浏览器
crypto.randomUUID()/getRandomValues,Nodecrypto.randomUUID()),不要用Math.random; - 存储类型:UUID 在 PostgreSQL 用
uuid类型(16 字节)比varchar(36)省一半空间;MySQL 可用BINARY(16); - 索引与写入:随机主键会造成页分裂与随机写;写入密集的表建议用 ULID 这类有时间前缀的有序 ID,或保留自增主键、UUID 只做业务唯一键;
- 大小写与格式:入库前统一转小写并去掉连字符,避免“视觉相同但比较不等”;
- 长度校验:接收外部 ID 时先用正则校验格式再查库,避免无效值直接打到索引。
对外暴露与隐私
对外标识建议使用无序值,避免暴露业务量(/order/1024 会告诉别人你有多少订单)与被遍历抓取。反过来,内部日志与排查更依赖可排序 ID,因此常见做法是:内部主键自增、对外暴露 UUID/ULID,二者用映射表关联。
常见误区
- 把 ULID 当“加密过的自增 ID”:它只编码时间,不含秘密,仍需鉴权;
- 用短 ID 做安全边界:短码可被枚举,必须配合速率限制与权限校验;
- 依赖“不会重复”而不建唯一索引:分布式系统里唯一约束是必需兜底;
- 混用多种 ID 却无文档:同一个资源在不同接口用不同 ID 形式,会让调用方反复踩坑,应统一并写进文档。
ID 在 URL 与日志中的取舍
- URL 中优先短且 URL 安全:NanoID 或去连字符的十六进制比带花括号的 UUID 更友好;
- 日志中要可搜索:打印完整 ID 便于精确检索,截断会导致无法关联;
- 不要把 ID 当权限:知道 ID 不等于有权访问,鉴权必须独立判断;
- 敏感场景避免可枚举:顺序号可被遍历,若必须用顺序值,应配合访问控制与限速。
可读性与纠错
需要人工抄写或口述的 ID,应去掉易混淆字符(0/O、1/l/I)并使用分组显示;NanoID 支持自定义字符集正好可用。若必须人工输入,建议加校验位,能把绝大部分抄错在提交前拦下。
批量生成的注意点
一次生成大量 ID 时,注意两点:一是不要用同一个时间戳做多份 ULID 的时间前缀(同毫秒内应使用递增随机部分),否则排序会退化;二是生成后应做去重校验,尤其在多实例并发生成时,唯一索引是最后防线。