UUID 使用场景
UUID 在实际生产中有许多适合的使用场景。让我们看看哪些场景最适合使用 UUID。
1. 消息追踪 ID
场景描述:在微服务架构中追踪一个请求的完整调用链。
为什么适合:
✅ 全局唯一:每个请求有唯一 ID
✅ 无需协调:各服务独立生成
✅ 生成速度快:不影响请求性能
✅ 可携带:在 HTTP Header 中传递
实现示例:
2. 会话 ID
场景描述:Web 应用中标识用户会话。
为什么适合:
✅ 随机性强:难以被猜测
✅ 安全性高:不易被攻击
✅ 无需持久化:临时使用
✅ 无需有序:会话无需排序
实现示例:
3. 日志追踪 ID
场景描述:在海量日志中快速定位特定请求的所有日志。
为什么适合:
✅ 全局唯一:精确匹配
✅ 生成快:不影响日志性能
✅ 可搜索:便于索引和查询
✅ 可关联:关联到业务 ID
实现示例:
4. 分布式事务 ID
场景描述:在分布式事务中标识一个全局事务。
为什么适合:
✅ 全局唯一:避免事务冲突
✅ 无需协调:各服务独立生成
✅ 可传递:在服务间传递
✅ 可追溯:追踪事务流程
实现示例:
5. 临时文件名
场景描述:生成临时文件名,避免冲突。
为什么适合:
✅ 唯一性:文件名不冲突
✅ 随机性:不易被猜测
✅ 临时性:短期使用即可
✅ 无需有序:临时文件无需排序
实现示例:
6. API 请求 ID
场景描述:API 中标识每个请求。
为什么适合:
✅ 唯一标识:精确匹配请求
✅ 问题追踪:快速定位问题
✅ 防重放:防止请求重放
✅ 无需有序:请求无需排序
实现示例:
7. 测试数据 ID
场景描述:在测试环境中生成测试数据。
为什么适合:
✅ 快速生成:无需手动创建
✅ 唯一性:避免冲突
✅ 无需有序:测试数据无需排序
✅ 临时性:测试完成后清理
实现示例:
8. 数据库外键
场景描述:作为外键关联其他表。
为什么可以使用:
✅ 全局唯一:不会冲突
✅ 分布式:多表独立
⚠️ 无序:可能影响性能
⚠️ 存储大:占用空间
使用建议:
✅ 适合:关联表、日志表
❌ 不适合:主表、核心表
9. 缓存键
场景描述:在缓存系统中作为键。
为什么适合:
✅ 唯一性:键不冲突
✅ 分布式:多节点缓存
✅ 随机性:缓存分布均匀
实现示例:
❌ 订单号
原因:
- 需要可读性
- 需要有序性
- 需要短小
推荐:号段模式
❌ 用户 ID
原因:
- 需要有序性
- 需要短小
- 需要便于查询
推荐:雪花算法
❌ 支付流水号
原因:
- 需要可追溯
- 需要有序
- 需要安全性
推荐:号段模式 + 加密
使用 UUID 的检查清单
- 需要全局唯一?
- 有序性不重要?
- 生成速度要求高?
- 无需中央协调?
- 无需持久化排序?
- 存储空间充足?
如果以上都满足,那么 UUID 是不错的选择。
1. 使用合适的版本
UUID 有多个版本,选错版本会让本来能解决的问题重新出现。判断依据只有两个:要不要有序,要不要跨节点协调。
| 场景 | 推荐版本 | 理由 |
|---|---|---|
| 主键、订单号等需要有序的场景 | UUID v7 | 按时间排序,写入不会产生随机写,索引不会频繁分裂 |
| 追踪 ID、会话 ID、临时文件名 | UUID v4 | 完全随机,不需要协调,碰撞概率可以忽略 |
| 由名称派生的稳定 ID | UUID v5 | 同样的命名空间和名称永远得到同一个值 |
| 需要嵌入时间的场景 | UUID v1 | 含时间戳和节点信息,但会暴露 MAC 地址 |
| 需要嵌入用户身份的 DCE 安全场景 | UUID v2 | 少见,了解即可 |
Java 实现:
// v4:完全随机,最通用
UUID randomId = UUID.randomUUID();
// v7:时间有序,标准库还没有,需要第三方库
// 依赖:com.fasterxml.uuid:java-uuid-generator
UUID timeOrderedId = Generators.timeBasedEpochFirst().generate();
Python 实现:
import uuid
# v4:完全随机
random_id = uuid.uuid4()
# v7:时间有序,需要 Python 3.14 及以上
ordered_id = uuid.uuid7()
选版本有一条硬规则:不要为了「看起来更随机」去用 v1。v1 里带着节点信息,泄露出去等于暴露机器的 MAC 地址;v4 一样唯一,却不含任何可以被反推的信息。
2. 考虑存储格式
UUID 是 128 位,存成什么格式决定了它占多少空间、能不能直接当索引键。常见的三种存法差别很大:
| 存储格式 | 占用空间 | 能否直接排序 | 说明 |
|---|---|---|---|
| CHAR(36) | 36 字节 | 能,按字符串比较 | 可读性最好,索引体积最大 |
| BINARY(16) | 16 字节 | 能,按字节比较 | 空间最省,程序里要做转换 |
| BIGINT 拆分存储 | 16 字节 | 需要自行处理 | 拆成高低位两列,使用麻烦 |
一亿行的表,CHAR(36) 和 BINARY(16) 相差 2GB 索引体积;再叠加聚簇索引,实际写入量和内存占用都会跟着涨。
格式转换:
/** UUID 转成 16 字节的二进制形式。 */
public static byte[] toBytes(UUID uuid) {
ByteBuffer buffer = ByteBuffer.allocate(16);
buffer.putLong(uuid.getMostSignificantBits());
buffer.putLong(uuid.getLeastSignificantBits());
return buffer.array();
}
/** 16 字节的二进制形式转回 UUID。 */
public static UUID toUuid(byte[] bytes) {
ByteBuffer buffer = ByteBuffer.wrap(bytes);
return new UUID(buffer.getLong(), buffer.getLong());
}
存储定义:
CREATE TABLE orders (
id BINARY(16) NOT NULL,
name VARCHAR(64) NOT NULL,
PRIMARY KEY (id)
);
要避开的一个坑:用 BINARY(16) 存 UUID v4 时,字节序是随机的,主键索引会不断分裂,写入性能下降。v7 的字节序本身就是时间序的,即使存成 BINARY(16) 也不会有这个问题。
工程上常见的折中是:对外接口和日志里用字符串(可读、可复制),数据库里用 BINARY(16)(省空间、快比较),转换只在边界上做一次。
3. 建立索引
数据设计要点
- 索引服务于高频查询,重点是缩小扫描范围,而不是堆更多字段。
- 查询目标是快速定位状态、任务或资源,避免在关键路径上做大范围扫描。
4. 避免作为主键
数据设计要点
- 核心是在
orders里保存业务事实,而不是把规则散落在应用逻辑里。- 索引服务于高频查询,重点是缩小扫描范围,而不是堆更多字段。
- 关键字段包括
id、order_uuid,它们决定后续查询和管理能力。
何时选择 UUID
选择 UUID:
✅ 消息追踪
✅ 会话管理
✅ 日志追踪
✅ 临时数据
✅ 测试数据
不选择 UUID:
❌ 订单号
❌ 用户 ID
❌ 支付流水
❌ 商品 ID
❌ 核心业务 ID
了解了 UUID 的使用场景后,我们学习 UUID 的优化方案。
👉 下一节:优化方案
发现内容有问题?
告诉我们哪里需要改进。
