百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

在 MySQL 中使用 UUID 作为主键的存在问题及如何优化?

csdh11 2025-03-26 11:13 20 浏览

在分布式架构中,UUID(通用唯一标识符)因其能够确保全球唯一性而广泛应用。它不依赖于数据库的自增机制,特别适合于多个系统间的数据同步。然而,尽管 UUID 提供了很多优势,直接使用它作为 MySQL 表的主键可能会带来性能上的问题。

本文将详细探讨在 MySQL 中使用 UUID 作为主键的缺点,并分享一些优化方法,以尽量减少其对性能和存储的影响。

UUID 版本简介

UUID 目前有多个版本,其中不同的版本有不同的特性和用途。理解这些版本有助于我们选择最适合的 UUID 形式,以减轻性能负担。

UUID v1:基于时间的 UUID

UUID v1 使用当前时间戳和硬件地址(如 MAC 地址)来生成唯一标识符。它的特点是包含了时间信息,适合用作按时间顺序生成的唯一 ID。

虽然许多现代计算使用 UNIX 纪元时间(1970 年 1 月 1 日)作为基础,但 UUID 实际上使用不同的日期 1568 年 10 月 10 日,这是公历开始得到更广泛使用的日期。UUID 中嵌入的时间戳从该日期开始以 100 纳秒为增量增长,然后用于设置 UUID 的 time_lowtime_midtime_hi 段。

UUID 的第三段包含 version 以及 time_hi 并占据该段的第一个字符。对于所有版本的 UUID 都是如此,如后续示例所示。 reserved 部分也称为 UUID 的变体,它决定如何使用 UUID 中的位。最后,UUID 的最后一段是 node ,它是生成 UUID 的系统的唯一地址。

UUIDv2:基于 POSIX 用户 ID

UUID v2 在 v1 的基础上做了修改,使用 POSIX 用户 ID 替代了时间部分。这个版本较少被使用,因其增加了冲突的可能性。

UUID v3 和 v5:基于名称的 UUID

这两个版本的 UUID 通过对命名空间和名称进行哈希(v3 使用 MD5,v5 使用 SHA1)来生成唯一标识符。它们适用于生成可预测的、基于相同输入数据的 UUID。

UUID v4:随机 UUID

UUID v4 是最常见的 UUID 版本,完全基于随机数生成。它的优势在于生成简单、没有时间信息,但由于其完全随机性,可能会对数据库索引产生较大影响。

UUIDv6:时间戳优先 UUID

UUIDv6 与 UUIDv1 几乎相同,唯一的区别在于它对时间戳的存储方式做了调整。具体来说,UUIDv6 将时间戳的最重要部分放在前面,而不是像 UUIDv1 那样将其放在后面。这样做的目的是为了更好地排序,并且使生成的 UUID 更加适用于数据库等需要快速插入的场景。

下图展示了这两种版本的差异。

通过这种方式,UUIDv6 在保留与 UUIDv1 兼容性的同时,也提升了排序性能,因为时间戳的最重要部分被优先存储。

UUIDv7:基于 Unix 时间戳的 UUID

UUIDv7 也是基于时间戳的 UUID 变体,但它使用了更常见的 Unix Epoch 时间戳,而不是 UUIDv1 中使用的公历日期。与 UUIDv1 相比,UUIDv7 的另一个关键区别是其节点部分,UUIDv7 不再使用基于硬件地址的节点,而是用随机值替代。这使得 UUIDv7 更加难以追溯到生成它的系统,从而提高了隐私性。

UUIDv8:供应商特定的 UUID

UUIDv8 是目前最新的 UUID 版本,它允许特定于供应商的实现,同时仍然遵循 RFC 标准。UUIDv8 的要求与其他版本类似,在其第三段的第一个位置明确指定版本号。不同的是,UUIDv8 的设计更灵活,可以根据需求进行自定义,适用于特定的使用场景。

UUID 在 MySQL 中的挑战

尽管 UUID 在分布式系统中具有全球唯一性的优势,但直接将 UUID 用作 MySQL 主键时,可能会带来以下问题:

1. 性能问题:Insert 性能下降

MySQL 中的主键默认会创建索引,通常使用 B+ 树结构。每次插入数据时,主键索引都会更新,保证数据按顺序排列。对于自动递增的整数主键,数据插入是按顺序进行的,B+ 树的结构不会频繁发生重平衡。然而,UUID 是随机的,这意味着每次插入时,MySQL 都需要调整 B+ 树结构,频繁发生页面拆分(page split),导致性能下降。

每当一条新记录插入到 MySQL 的表中,与主键关联的索引都需要更新,以便查询表的性能。MySQL 中的索引采用 B+ 树的形式,这是一种多层数据结构,允许查询快速找到所需的数据。

下图演示了此结构的相对简单版本,其中有 6 个条目,值从 1 到 6。如果查询请求 5 ,MySQL 将从根节点开始,并从那里知道:它必须沿着树的右侧遍历才能找到它要找的东西。

为简单起见,这些图显示 B 树而不是 B+ 树。主要区别在于,在 B+ Tree 中,叶节点包含对实际数据的引用,而在 B-Tree 中,叶节点不包含对实际数据的引用。

如果添加值 7-9,MySQL 将拆分右侧节点并重新平衡树。

这个过程称为页拆分,目标是保持 B+ Tree 结构平衡,以便 MySQL 能够快速找到它要查找的数据。对于顺序值,这个过程相对简单;然而,当算法中引入随机性时,MySQL 重新平衡树可能需要更长的时间。在大容量数据库上,这可能会损害用户体验,因为 MySQL 会尝试保持树平衡。

2.更高的存储利用率

MySQL 中的所有主键均已建立索引。默认情况下,自动递增整数每个值将消耗 32 位存储空间。将此与 UUID 进行比较。如果以紧凑的二进制形式存储,单个 UUID 将占用磁盘上的 128 位。这已经是 32 位整数消耗的 4 倍。相反,如果您选择使用更易读的基于字符串的表示形式,则每个 UUID 都可以存储为 CHAR(36) ,每个 UUID 消耗高达 288 位的数据。这意味着每条记录将存储比 32 位整数多 9 倍的数据。

除了在主键上创建的默认索引外,二级索引也会消耗更多的空间。这是因为二级索引使用主键作为指向实际行的指针,这意味着它们需要与索引一起存储。这可能会导致数据库的存储要求显著增加,具体取决于使用 UUID 作为主键的表上创建的索引数量。

最后,页面分割(如上一节所述)也会对存储利用率和性能产生负面影响。InnoDB 假设主键将按数字或字典顺序按可预测的方式递增。如果为 true,InnoDB 将在创建新页面之前将页面填充到页面大小的 94% 左右。当主键是随机的时,每个页面所使用的空间量可以低至 50%。因此,使用包含随机性的 UUID 可能会导致过度使用页面来存储索引。

在 MySQL 中使用 UUID 主键的最佳方法

如果您绝对需要使用 UUID 作为表中记录的唯一标识符,您可以遵循一些最佳实践,以最大程度地减少这样做的负面影响。

1. 使用二进制数据类型

虽然 UUID 通常表示为 36 个字符的字符串,但它也可以以其本机二进制格式存储。如果将 UUID 转换为二进制值,可以将其存储在 BINARY(16) 列中,这将每个 UUID 的存储要求减少到 16 个字节。这仍然比 32 位整数大一些,但比将 UUID 存储为 CHAR(36) 更为高效。

create table uuids(
  UUIDAsChar char(36) not null,
  UUIDAsBinary binary(16) not null
);

insert into uuids set
  UUIDAsChar = 'd211ca18-d389-11ee-a506-0242ac120002',
  UUIDAsBinary = UUID_TO_BIN('d211ca18-d389-11ee-a506-0242ac120002');

select * from uuids;
-- +--------------------------------------+------------------------------------+
-- | UUIDAsChar                           | UUIDAsBinary                       |
-- +--------------------------------------+------------------------------------+
-- | d211ca18-d389-11ee-a506-0242ac120002 | 0xD211CA18D38911EEA5060242AC120002 |
-- +--------------------------------------+------------------------------------+

2. 使用有序的 UUID 变体

使用支持排序的 UUID 版本可以使生成的值更加连续,从而减少前面提到的页面拆分问题,从而减轻使用 UUID 带来的性能和存储负担。即使这些 UUID 是在多个系统上生成的,基于时间的 UUID(例如版本 6 或 7)仍能保证唯一性,并保持尽可能的顺序性。UUIDv1 是个例外,它的最低有效部分首先包含时间戳。

3. 使用内置的 MySQL UUID 函数

MySQL 支持在 SQL 中直接生成 UUID,但它只支持 UUIDv1 类型。尽管单独使用它们并不理想,但 MySQL 提供了一个名为 uuid_to_bin 的辅助函数。该函数不仅可以将 UUID 字符串转换为二进制格式,还可以使用 "swap 标志" 重新排序时间戳部分,使生成的二进制 UUID 更加连续。

set @uuidvar = 'd211ca18-d389-11ee-a506-0242ac120002';
-- Without swap flag
SELECT HEX(UUID_TO_BIN(@uuidvar)) as UUIDAsHex;
-- +----------------------------------+
-- | UUIDAsHex                        |
-- +----------------------------------+
-- | D211CA18D38911EEA5060242AC120002 |
-- +----------------------------------+

-- With swap flag
SELECT HEX(UUID_TO_BIN(@uuidvar,1)) as UUIDAsHex;
-- +----------------------------------+
-- | UUIDAsHex                        |
-- +----------------------------------+
-- | 11EED389D211CA18A5060242AC120002 |
-- +----------------------------------+

4. 使用备用 ID 类型

UUID 并不是唯一性的唯一标识符。在分布式架构中,已经有其他标识符类型被提出并得到广泛应用,如 Snowflake ID、ULID,甚至 NanoID(我们在 PlanetScale 中使用的就是这种 ID)。这些替代方案有时在性能和存储上会优于传统的 UUID。

# Snowflake ID
7167350074945572864

# ULID
01HQF2QXSW5EFKRC2YYCEXZK0N

# NanoID
kw2c0khavhql

结论

在 MySQL 中使用 UUID 作为主键可以(几乎)保证分布式系统中的唯一性,但这也伴随着一定的权衡。幸运的是,有多种 UUID 变体和替代方案可以帮助解决这些问题。

参考:https://planetscale.com/blog/the-problem-with-using-a-uuid-primary-key-in-mysql

相关推荐

探索Java项目中日志系统最佳实践:从入门到精通

探索Java项目中日志系统最佳实践:从入门到精通在现代软件开发中,日志系统如同一位默默无闻却至关重要的管家,它记录了程序运行中的各种事件,为我们排查问题、监控性能和优化系统提供了宝贵的依据。在Java...

用了这么多年的java日志框架,你真的弄懂了吗?

在项目开发过程中,有一个必不可少的环节就是记录日志,相信只要是个程序员都用过,可是咱们自问下,用了这么多年的日志框架,你确定自己真弄懂了日志框架的来龙去脉嘛?下面笔者就详细聊聊java中常用日志框架的...

物理老师教你学Java语言(中篇)(物理专业学编程)

第四章物质的基本结构——类与对象...

一文搞定!Spring Boot3 定时任务操作全攻略

各位互联网大厂的后端开发小伙伴们,在使用SpringBoot3开发项目时,你是否遇到过定时任务实现的难题呢?比如任务调度时间不准确,代码报错却找不到方向,是不是特别头疼?如今,随着互联网业务规模...

你还不懂java的日志系统吗 ?(java的日志类)

一、背景在java的开发中,使用最多也绕不过去的一个话题就是日志,在程序中除了业务代码外,使用最多的就是打印日志。经常听到的这样一句话就是“打个日志调试下”,没错在日常的开发、调试过程中打印日志是常干...

谈谈枚举的新用法--java(java枚举的作用与好处)

问题的由来前段时间改游戏buff功能,干了一件愚蠢的事情,那就是把枚举和运算集合在一起,然后运行一段时间后buff就出现各种问题,我当时懵逼了!事情是这样的,做过游戏的都知道,buff,需要分类型,且...

你还不懂java的日志系统吗(javaw 日志)

一、背景在java的开发中,使用最多也绕不过去的一个话题就是日志,在程序中除了业务代码外,使用最多的就是打印日志。经常听到的这样一句话就是“打个日志调试下”,没错在日常的开发、调试过程中打印日志是常干...

Java 8之后的那些新特性(三):Java System Logger

去年12月份log4j日志框架的一个漏洞,给Java整个行业造成了非常大的影响。这个事情也顺带把log4j这个日志框架推到了争议的最前线。在Java领域,log4j可能相对比较流行。而在log4j之外...

Java开发中的日志管理:让程序“开口说话”

Java开发中的日志管理:让程序“开口说话”日志是程序员的朋友,也是程序的“嘴巴”。它能让程序在运行过程中“开口说话”,告诉我们它的状态、行为以及遇到的问题。在Java开发中,良好的日志管理不仅能帮助...

吊打面试官(十二)--Java语言中ArrayList类一文全掌握

导读...

OS X 效率启动器 Alfred 详解与使用技巧

问:为什么要在Mac上使用效率启动器类应用?答:在非特殊专业用户的环境下,(每天)用户一般可以在系统中进行上百次操作,可以是点击,也可以是拖拽,但这些只是过程,而我们的真正目的是想获得结果,也就是...

Java中 高级的异常处理(java中异常处理的两种方式)

介绍异常处理是软件开发的一个关键方面,尤其是在Java中,这种语言以其稳健性和平台独立性而闻名。正确的异常处理不仅可以防止应用程序崩溃,还有助于调试并向用户提供有意义的反馈。...

【性能调优】全方位教你定位慢SQL,方法介绍下!

1.使用数据库自带工具...

全面了解mysql锁机制(InnoDB)与问题排查

MySQL/InnoDB的加锁,一直是一个常见的话题。例如,数据库如果有高并发请求,如何保证数据完整性?产生死锁问题如何排查并解决?下面是不同锁等级的区别表级锁:开销小,加锁快;不会出现死锁;锁定粒度...

看懂这篇文章,你就懂了数据库死锁产生的场景和解决方法

一、什么是死锁加锁(Locking)是数据库在并发访问时保证数据一致性和完整性的主要机制。任何事务都需要获得相应对象上的锁才能访问数据,读取数据的事务通常只需要获得读锁(共享锁),修改数据的事务需要获...