在软件开发、数据库管理和分布式系统中,唯一标识符(Unique Identifier)是一个至关重要的概念。我们常常会为实体设计唯一的ID,以保证其在系统中的唯一性,避免实体冲突。自增ID、UUID等唯一标识符便在这样的需求下应运而生。

什么是UUID?

UUID(Universally Unique Identifier, 通用唯一识别码)由RFC 4122定义,技术上等同于 ITU-T Rec. X.667 | ISO/IEC 9834-8,最早由开放软件基金会标准化。

标准格式

UUID 的 16 个 8 位字节表示为 32 个十六进制数字,由连字符 '-' 分隔成五组显示,形式为“8-4-4-4-12”总共 36 个字符(32 个十六进制数字和 4 个连字符)。

  • UUID要求以小写形式生成字符,同时对输入不区分大小写。
    形如:xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
名称字节字长(16进制数字码长)说明
time_low48整数:低位 32 bits 时间戳
time_mid24整数:中间位 16 bits 时间戳
time_hi_and_version24最高有效位中的 4 bits“版本”(Mxxx),后面是高 12 bits 的时间戳
clock_seq_hi_and_res clock_seq_low24最高有效位为 1-3 bits“变体”(Nxxx),后跟13-15 bits 时钟序列
node61248 bits 节点 ID

我们重点关注其中的版本变体

变体 variant

变体(variant)字段占位1~3bit,RFC 4122共规定了4种变体。(x代表位置没有意义)

  • 变体 0 (形如0xxx), 用于向后兼容已经过时的1988年开发的 Apollo 网络计算系统(NCS)1.5 UUID 格式;
  • 变体 1 (形如10xx), 按照大端序作为二进制存储与传输,RFC称“保留,微软公司向后兼容;
  • 变体 2 (形如110x), 按照小端序作为二进制存储与传输;
  • 变体 3 (形如111x), 保留未使用。

在RFC 4122中,我们实际关注的是变体1(10xx)这一类别,所以我们在变体(Nxxx)这一字段事实上只能见到四个值:

1000 - 8
1001 - 9
1010 - a
1011 - b

版本 version

版本可以理解为变体下不同的子类型,RFC 4122中定义了五个版本,版本(Mxxx)取值为1/2/3/4/5.

版本1 日期时间和MAC地址

  • 时间戳版本,60-bit 的时间戳和节点的48-bit MAC地址而生成的;
    • 优点:基于时间戳和MAC地址生成,保证了UUID唯一性;
    • 缺点:有暴露节点MAC隐私信息的风险

版本2 日期时间和DCE标识符

  • 在版本1的基础上使用"DCE安全标识符"
    • 优点:替换掉了MAC地址,解决了暴露计算机隐私信息的问题;
    • 缺点:DCE实现在RFC 4122中未提及,标识符对生成速率有影响

版本3/5 散列命名空间

  • 通过将命名空间(例如域名)和一个名字组合并生成哈希值来创建UUID。任何所需的UUID都可以用作命名空间指示符。
    • 版本3 - MD5
    • 版本5 - SHA1

版本4 随机

  • 一个随机或伪随机生成的60位值。RFC 4122 建议“在各种主机上生成 UUID 的分布式应用程序必须愿意依赖所有主机上的随机数源。如果这不可行,则应使用名称空间变体。”

应用场景

  • 数据库键值:UUID 通常用作数据库表中的唯一键,MySQL、SQL Server、PostageSQL等DBMS都提供了不同的UUID函数;
  • 分布式系统:在没有中央协调器的情况下,确保唯一标识符;
  • 软件构建:在构建过程中生成唯一的组件或版本标识符;
  • 文件系统:用于标识文件或目录的唯一性。

一些问题

  • 大小问题:相对于简单的ID标识符,UUID虽然提供了极低的唯一性,但也因其128位的长度占用了相对较大的空间;
    • 如数据库,使用UUID作为主键会导致索引体积增大;
  • 可排序性问题:UUID的生成方式(尤其是版本4随机生成)导致它们在时间上是无序的;
    • 数据库索引:无序的UUID会导致数据库索引碎片化,进而降低查询性能;
    • 日志记录:无序的UUID可能导致难以按时间顺序排序或筛选记录;
  • 可预测性问题:某些版本的UUID(如版本1)可能会泄露生成时间和生成设备的信息,从而存在安全隐患。
  • 重复问题:尽管UUID设计上是唯一的,但并不能完全排除重复的可能性;
    • 生成算法设计不合理;
    • 依赖MAC、ID等选取不合理;
    • 随机数碰撞;

一些解决问题的方案

  • 使用BINARY(16)而不是CHAR(36)来存储UUID;
  • 使用有序UUID(如时间UUID)来确保在时间上的排序性,从而提高索引和查询性能;
  • 对于需要短而可读标识符的场景,可以考虑使用短UUID;

UUID 应用实现

Python

import uuid

# 生成一个UUID(版本4)
unique_id = uuid.uuid4()
print(unique_id)

MySQL

方案1:调用UUID()函数:

INSERT INTO my_table (id, name)
VALUES (UUID(), 'Example Name');

方案2:触发器实现

-- 这个触发器会在每次插入数据之前检查`id`是否为空,如果是,则生成一个新的UUID。
DELIMITER //

CREATE TRIGGER before_insert_my_table
BEFORE INSERT ON my_table
FOR EACH ROW
BEGIN
    IF NEW.id IS NULL THEN
        SET NEW.id = UUID();
    END IF;
END;

//

DELIMITER ;

-- 调用
INSERT INTO my_table (name) VALUES ('Example Name');

更多方案

正如我们一开始提到的,UUID事实上要解决的是我们在分布式系统、软件开发和数据库设计等场景下的唯一标识问题。不同的场景下,我们应选取不同的标识符方案,并不一定要因为使用UUID而妥协,标识符的规范应当视场景而定。

由此,我们还有很多针对不同场景下的ID方案:

  • 自增ID;
  • 一定精度的时间戳;
  • 有序UUID;
  • NanoID
  • Snowflake 雪花算法;

参考阅读