
用 UUID 当 MySQL 主键上线半年后为什么写入性能掉一半很多人觉得分布式场景下直接用 UUID 当数据库主键省事既不用考虑自增 ID 耗尽也不用担心分库分表时的冲突。如果你把它用在 MySQL 的 InnoDB 引擎里刚上线可能没什么感觉。但几个月后数据量稍微起来一点写入性能就会明显下降甚至频发慢 SQL 报警。这其实和 InnoDB 的底层设计有关。聚簇索引的脾气InnoDB 表的数据是按主键顺序存放在 B 树的叶子节点上的这就是聚簇索引。当你插入一条数据时InnoDB 会根据主键的值找到合适的位置。如果主键是趋势递增的比如自增 ID 或者雪花算法新数据总是一条条往后追加。一个默认 16KB 的数据页写满了就开一个新页。这个过程非常顺畅磁盘主要是顺序写入。但 UUID 完全不同它是无序的。上一条主键可能是550e8400...下一条突然变成了1a2b3c4d...。这就导致新插入的数据无法顺序追加InnoDB 被迫去 B 树的中间某个数据页寻找位置。页分裂与碎片陷阱假设系统找到了那个应该插入 UUID 的数据页但很不巧这个 16KB 的页已经塞满了。这就引发了麻烦的“页分裂”。InnoDB 必须申请一个新的数据页把满页里大约一半的数据搬过去再把新数据插进去。这里有两个代价第一是产生碎片。原来紧凑的 16KB 数据页被硬生生切成了两半空间利用率瞬间掉到 50% 左右。如果持续乱序插入表里会充斥着大量半空的碎片页。第二是随机 I/O。为了维护树的平衡树结构被频繁调整。原本只要在文件尾部写数据现在变成了在磁盘上到处乱跳的随机写入。如果业务并发量大这种频繁的页分裂会让 MySQL 把大量的 CPU 和磁盘 I/O 耗在调整 B 树结构上直观表现就是写入延迟飙升。不光是写得慢。由于数据在磁盘上被打散那些本来能利用局部性原理提升效率的范围查询比如按创建时间捞数据也因为要在不同的物理数据页之间跳跃变得慢得出奇。怎么解决分布式主键确实是刚需但没必要死磕 UUID 字符串。方案一换用雪花算法Snowflake这是目前最通用的做法。雪花算法生成的 64 位整数高位是时间戳低位是机器码和序列号。它不仅全网唯一而且在时间上是趋势递增的。对 InnoDB 来说它长得很像自增 ID能规避页分裂问题。8 字节的 bigint 比 36 字节的 UUID 字符串省空间得多每一页能存的索引和数据更多树的高度更低查询更快。方案二必须用 UUID 时做个小转换有些老旧系统或者特殊场景非得用 UUID 不可。如果你用的 MySQL 8.0 以上版本可以用UUID_TO_BIN()函数把 36 字节的 UUID 字符串转成 16 字节的 BINARY。更关键的是它可以把 UUID 里代表时间戳的位交换到前面硬生生把乱序的 UUID 变成趋势递增的值。分布式唯一性很重要但顺着数据库的底层机制顺毛摸更重要。下回建表再遇到 UUID 主键记得先拦下来。