制作主键的两种方式
大多数表都需要为每一行配一个唯一键。两种常见选择是数据库的自增整数(1、2、3……)和一个 UUID。整数小且天然有序;UUID 可以在任何地方、由任何人生成,无需协调。哪个更好取决于你看重什么,而答案有一个容易被忽视的性能维度。
团队为何转向 UUID
- 无需协调。 任何服务、客户端或离线设备都能铸造一个不会冲突的 UUID,因此你无需中央序列,也无需往返数据库去取一个 id。这在分布式系统中、以及在一行保存之前就要生成 id 时,是无价的。
- 不可猜测。 顺序整数 id 会泄露信息(存在多少订单,以及下一个的 id),并让攻击者通过递增遍历记录。一个随机 UUID 不暴露其中任何一点。
- 利于合并。 来自不同系统的数据合并时不会发生 id 冲突。
代价是大小,16 字节对整数的 4 或 8 字节,这很重要,因为主键会被复制进每个二级索引和每个引用该行的外键。
随机键的隐藏代价
还有一个更微妙、更重要的代价。数据库把行存储在一个按主键排序的索引中(通常是一棵 B 树)。用顺序键时,每个新行都追加到索引末尾,因此活跃页留在内存中,写入很廉价。用随机的 v4 UUID 时,每次插入都落在任意位置。数据库必须读取、修改并写入散布于整个索引各处的页,造成页分裂、碎片化以及多得多的缓存未命中。在一张大型、写入繁重的表上,随机键能可测量地拖慢插入并使索引膨胀,而这一切都没有任何错误可供指认。
v7 为何改变这笔账
这正是 UUID 版本 7 旨在解决的问题。通过把一个毫秒级时间戳放在最高有效位,v7 值是按时间有序的:新行插入在索引末尾附近,正如一个顺序整数那样,同时保留 UUID 去中心化、无需协调的生成方式。你获得了自增键的索引局部性收益的大部分,却无须中央序列。对于想要 UUID 键的新系统而言,v7 通常正是因为这个原因而成为正确的版本。
实务要点
- 把 UUID 存为原生 UUID 或二进制类型,而非一个 36 字符的字符串。 存储文本形式浪费空间并拖慢比较;二进制形式为 16 字节。
- 在插入性能要紧之处用 v7 作键,在你特意不希望 id 中嵌入任何时间信息之处用 v4。
- 对于一张永不需要外部生成 id 的单节点表,一个自增整数仍然完全够好;当分布式、不可预测性或预先生成要紧时,UUID 才显出其价值。
UUID 工具 同时生成 v4 和 v7,并解码一个 v7 值的嵌入时间戳,使你能直接看到那种有序性,全部在你的浏览器中。