
一、UUID 为什么需要升级?
你有没有遇到过这样的场景:
- 数据库表里存了几千万条记录,每次插入新数据都慢得让人抓狂
- 用户反馈说"你们的订单号太长了,我手机上都显示不全"
- 运维同学凌晨被叫醒排查问题,盯着日志里一堆
550e8400-e29b-41d4-a716-446655440000眼睛都花了
这些痛点,恰恰暴露了传统 UUID 的三大硬伤:
- 存储成本高:36 个字符(加上连字符)占用空间大,存储和传输都不够经济
- 索引性能差:UUID v4 完全随机,写入数据库时会把 B+ 树索引"撕碎",导致页分裂频繁
- 可读性糟糕:一串十六进制字符,人工核对容易出错,排查问题效率低
好消息是,近年来工程界推出了四种升级方案,分别从不同角度解决这些问题。接下来我们逐一拆解它们的技术原理和适用场景。
二、四种方案详解(含实战代码)
2.1 ULID:最均衡的选择
ULID(Universally Unique Lexicographically Sortable Identifier)可以理解为"能按时间排序的 UUID"。它的设计哲学很简单:既要兼容现有 UUID 生态,又要解决排序和长度问题。
核心设计
ULID 采用 128 位结构,分为两部分:
- 前 48 位:毫秒级时间戳(可用到 10889 年)
- 后 80 位:加密安全的随机数
最终通过 Crockford Base32 编码成 26 个字符 ,比传统 UUID 短了 28%。更妙的是,它刻意排除了容易混淆的字符(I、L、O、U),大小写不敏感。
实战代码
python
# 需要安装 ulid-py(注意不是 ulid)
import ulid
from datetime import datetime
# 生成一个 ULID
new_id = ulid.new()
print(f"生成的 ULID: {new_id}")
# 输出示例:01J4X3ZQ8Y6S2F9D7C3A1B0E5G
# 解析时间戳(验证排序能力)
readable_time = new_id.timestamp().datetime
print(f"生成时间: {readable_time}")
# 输出示例:生成时间: 2024-03-15 14:23:45.123000
# 批量生成验证字典序
ids = [ulid.new().str for _ in range(5)]
print("排序前:", ids)
print("排序后:", sorted(ids))
# 排序后的顺序与生成时间一致
# 注意这里的输出并不是完全一致的!!!因为 1ms 内生成的数据排序是有随机性的
# 若需严格单调递增,可使用如下代码生成
# from ulid.api import monotonic
# monotonic.new()
关于和 UUID 的兼容性说明
第一眼可能会有这样的疑问
- UUID 示例:
4876417f-d6d1-45af-9a71-36dc64dc2eaa - ULID 示例:
01KE8M4KTXG7AYTY0K5B82KZ7G
为何会说二者兼容呢?
核心原因是
- UUID:128 bits 进行 base16 编码
- ULID:128 bits 进行 base32 编码
而数据库中,MySQL 支持 binary 数据类型,PostgreSQL 直接支持 uuid 类型,所以说在存储上,二者是兼容的
更进一步,ulid 库提供了 ulid 直接转换为 uuid 的接口:
python
import ulid
print(ulid.new().uuid)
# 019b914d-e446-1989-7c33-9e25d155e641
优劣势分析
优势:
- 完全兼容 UUID 字段,可以直接存入 PostgreSQL 的
UUID类型 - 支持字典序排序,数据库插入时索引写入集中在尾部,避免页分裂
- URL 安全,不含特殊字符
劣势:
- 同一毫秒内大量并发插入时,可能出现索引热点(不过实际场景中很少遇到)
- Base32 编码比 Base64 稍长(但已经比 UUID 短很多)
适用场景:需要兼容现有 UUID 系统,同时希望优化数据库索引性能的场景,比如订单表、用户表的主键。
2.2 CUID:分布式系统的小而美方案
CUID(Collision-resistant Unique Identifier)的设计目标很明确:在分布式环境下生成短小精悍的唯一 ID。它通过"时间戳 + 机器标识 + 进程 ID + 计数器"的组合,确保跨节点不会冲突。
核心设计
CUID 的结构包含四个部分:
- 时间戳:秒级或毫秒级(取决于实现)
- 机器指纹:通过主机名哈希生成
- 进程 ID:避免同一台机器多进程冲突
- 计数器:同一毫秒内递增
最终编码成 12 个字符(常见长度),是四种方案中最短的。
实战代码
python
from cuid import cuid
# 生成 CUID
order_id = cuid()
print(f"订单 ID: {order_id}")
# 输出示例:cljx3zk29001mv
# 验证分布式场景的唯一性
batch_ids = [cuid() for _ in range(10000)]
print(f"生成 10000 个 ID,重复数: {10000 - len(set(batch_ids))}")
# 输出:生成 10000 个 ID,重复数: 0
# 前缀识别(部分实现会以 'c' 开头,是 cuid 的标志)
print(f"是否以 'c' 开头: {order_id.startswith('c')}")
优劣势分析
优势:
- 长度最短,非常适合需要人工输入的场景(如客服查询订单号)
- 可读性强,字符集简单(0-9、a-z)
- 多线程、多进程环境下性能优秀
劣势:
- 不兼容 UUID 字段,需要用
VARCHAR存储 - 缺乏统一标准,不同语言的实现可能有差异
- 唯一性依赖机器标识,分布式部署时需要确保配置正确
适用场景:需要用户手动输入或展示的场景,比如优惠券码、邀请码、快递单号。
2.3 NanoID:极致性能的定制化方案
NanoID 的核心理念是:用最小的代价生成最快的 ID。它采用非加密级随机数,配合简洁的编码逻辑,性能远超其他方案。
核心设计
NanoID 默认使用 64 个 URL 安全字符(A-Za-z0-9_-),通过 21 个字符 达到与 UUID v4 相当的唯一性(约 2 126 2^{126} 2126 种可能)。
它的杀手锏是灵活定制:你可以自由调整长度、字符集,甚至可以生成纯数字 ID。
实战代码
python
from nanoid import generate
# 默认配置(21 字符)
session_id = generate()
print(f"会话 ID: {session_id}")
# 输出示例:V1StGXR8_Z5jdHi6B-myT
# 自定义长度(适合短链接)
short_url = generate(size=10)
print(f"短链接码: {short_url}")
# 输出示例:3x9K2bP7zQ
# 自定义字符集(纯数字 + 小写字母,大小写不敏感)
custom_id = generate(
size=16,
alphabet='0123456789abcdefghijklmnopqrstuvwxyz'
)
print(f"自定义 ID: {custom_id}")
# 输出示例:7z9x2b4c6d8e0f2a
# 性能测试
import time
start = time.time()
_ = [generate() for _ in range(100000)]
elapsed = time.time() - start
print(f"生成 10 万个 ID 耗时: {elapsed:.2f} 秒")
# 输出示例:生成 10 万个 ID 耗时: 0.12 秒
优劣势分析
优势:
- 性能最强,生成速度可达每秒数百万个
- 高度灵活,可根据场景定制长度和字符集
- URL 安全,适合作为参数传递
劣势:
- 默认配置大小写敏感,人工输入容易出错
- 不支持时间排序,需要额外字段记录创建时间
- 工具链生态不如 UUID 成熟
适用场景:高并发写入场景(如日志埋点、实时监控),或需要极致压缩长度的场景(如短链接、二维码)。
2.4 UUID v7:官方标准的稳妥选择
UUID v7 是 IETF 在 2024 年发布的 RFC 9562 标准,可以理解为"官方版 ULID"。它的核心价值在于:既享受时间排序的好处,又能无缝兼容现有 UUID 生态。
核心设计
UUID v7 保留了 128 位结构,但重新分配了各部分含义:
- 前 48 位:Unix 时间戳(毫秒)
- 12 位:随机或序列号(支持单调递增)
- 2 位 :版本标识(
0b10) - 4 位 :版本号(
7) - 后 62 位:随机数
实战代码
python
# pip install uuid7-standard
# 注意:需要 Python 3.8+,且会与旧的 uuid7 包冲突,需先卸载
import uuid7
from datetime import datetime, UTC
from uuid import UUID
# 生成 UUID v7
new_uuid = uuid7.create()
print(f"UUID v7: {new_uuid}")
# 输出示例:018e7d3f-9c2b-7000-8000-123456789abc
# 提取时间戳
timestamp = uuid7.time(new_uuid)
print(f"生成时间: {timestamp}")
# 输出示例:生成时间: 2024-03-15 14:23:45.123000+00:00
# 验证兼容性
parsed_uuid = UUID(str(new_uuid))
print(f"UUID 版本: {parsed_uuid.version}") # 输出:UUID 版本: 7
# 批量生成验证排序能力
uuids = [str(uuid7.create()) for _ in range(5)]
print("排序前:", uuids)
print("排序后:", sorted(uuids))
# 排序结果与生成时间一致
# 额外示例:指定时间戳生成
specific_time = datetime(2024, 1, 1, tzinfo=UTC)
uuid_at_time = uuid7.create(specific_time)
print(f"指定时间的 UUID: {uuid_at_time}")
优劣势分析
优势:
- 官方标准,长期支持有保障
- 完全兼容所有 UUID 工具链和数据库字段
- 支持时间排序,索引性能优秀
劣势:
- 长度最长(36 字符),存储成本最高
- 部分旧版本的编程语言库尚未支持生成
适用场景:大型系统迁移、金融合规场景、需要跨团队协作的项目(标准化避免扯皮)。
三、一张表看懂四种方案
| 维度 | ULID | CUID | NanoID | UUID v7 |
|---|---|---|---|---|
| 默认长度 | 26 字符 | 12 字符 | 21 字符 | 36 字符 |
| 大小写敏感 | 否 | 否 | 是(可改) | 否 |
| UUID 兼容 | ✅ 完全兼容 | ❌ 不兼容 | ❌ 不兼容 | ✅ 完全兼容 |
| 生成性能 | ~100 万/秒 | ~200 万/秒 | ~500 万/秒 | ~80 万/秒 |
| 时间排序 | ✅ 毫秒级 | ⚠️ 部分支持 | ❌ 不支持 | ✅ 毫秒级 |
| 可读性 | 高 | 极高 | 低 | 中 |
| 抗冲突性能 | ✅ 极高 | ✅ 高 | ✅ 极高 | ✅ 极高 |
| 可变长度 | ❌ 固定长度 | ❌ 固定长度 | ✅ 支持自定义 | ❌ 固定长度 |
| 核心优势 | 均衡兼顾 | 最短长度 | 极致性能 | 官方标准 |
| 典型应用 | 订单主键 | 优惠券码 | 日志埋点 | 大型系统迁移 |
四、场景化选型指南
🎯 快速决策树
是
否
追求性能
追求标准
需要人工输入
追求极致性能
需要短链接
是否需要兼容现有 UUID 系统?
优先级倾向
核心需求
ULID
UUID v7
CUID
NanoID
NanoID(自定义长度)
📋 典型场景推荐
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 数据库主键(新系统) | ULID | 索引友好 + 长度适中 |
| 数据库主键(旧系统升级) | UUID v7 | 零迁移成本 |
| 电商订单号 | CUID | 客服可手动输入查询 |
| 短链接服务 | NanoID | 自定义 8-10 字符即可 |
| 高并发日志系统 | NanoID | 性能最优,不需要排序 |
| 前端会话 ID | CUID/NanoID | 短小精悍,传输成本低 |
| 金融交易流水号 | UUID v7 | 官方标准,审计友好 |
| 物联网设备 ID | CUID | 存储资源受限 |
最后提醒一点:不要过度优化。如果你的系统日活只有几千人,传统 UUID v4 依然够用。技术选型的本质是在成本、收益和风险之间找平衡,而不是追求最新最酷的方案。
参考资料: