在本文中,我们将了解哪种 UUID(通用唯一标识符)类型最适合具有主键约束的数据库列。
虽然标准的 128 位随机 UUID 是一个非常受欢迎的选择,但您会发现这非常适合数据库主键列。
通用唯一标识符 (UUID) 是一个 128 位伪随机序列,可以独立生成,无需单个集中式系统负责确保标识符的唯一性。
RFC 4122 规范定义了UUID 的五个标准化版本,它们由各种数据库函数或编程语言实现。
例如,UUID()MySQL 函数返回版本 1 UUID 编号。
并且 JavaUUID.randomUUID()函数返回版本 4 UUID 编号。
对于许多开发人员来说,使用这些标准 UUID 作为数据库标识符非常有吸引力,因为:
- ids 可以由应用程序生成。因此不需要中央协调。
- 标识符冲突的可能性极低。
- id 值是随机的,您可以安全地将它发送到 UI,因为用户将无法猜测其他标识符值并使用它们来查看其他人的数据。
但是,出于多种原因,使用随机 UUID 作为数据库表主键不是一个好主意。
首先,UUID 很大。每条记录都需要 16 个字节作为数据库标识符,这也会影响所有关联的外键列。
其次,Primary Key 列通常有一个关联的 B+Tree 索引来加速查找或连接,B+Tree 索引按排序顺序存储数据。
然而,使用 B+Tree 索引随机值会导致很多问题:
- 索引页面将具有非常低的填充因子,因为这些值是随机出现的。因此,一个 8kB 的页面最终将只存储几个元素,因此在磁盘和数据库内存中浪费了大量空间,因为索引页面可以缓存在缓冲池中。
- 由于 B+Tree 索引需要重新平衡自身以保持其等距树结构,随机键值将导致更多的索引页拆分和合并,因为没有预先确定的填充树结构的顺序。
如果你使用的是 SQL Server 或 MySQL,那就更糟了,因为整个表基本上是一个聚集索引。
事实上,几乎所有数据库专家都会告诉您避免使用标准 UUID 作为数据库表主键:
TSID – 按时间排序的唯一标识符
如果您计划将 UUID 值存储在主键列中,那么您最好使用 TSID(按时间排序的唯一标识符)。
TSID Creator OSS 库提供了一种此类实现,它提供了一个由两部分组成的 64 位 TSID:
- 一个 42 位时间组件
- 一个 22 位随机分量
随机成分有两部分:
- 节点标识符(0 到 20 位)
- 一个计数器(2 到 22 位)
tsidcreator.node引导应用程序时,系统属性可以提供节点标识符:
-Dtsidcreator.node="12"
节点标识符也可以通过环境变量提供TSIDCREATOR_NODE:
export TSIDCREATOR_NODE="12"
该库在 Maven Central 上可用,因此您可以通过以下依赖项获取它:
<dependency> |
您可以创建一个Tsid最多可以使用 256 个节点的对象,如下所示:
Tsid tsid = TsidCreator.getTsid256();
从Tsid对象中,我们可以提取以下值:
64 位数值,
编码 64 位值的Crockford 的 Base32 字符串值,
存储在42-bit 序列中的纪元以来的 Unix 毫秒数
为了可视化这些值,我们可以将它们打印到日志中:
long tsidLong = tsid.toLong(); |
我们得到以下输出:
TSID numerical value: 388400145978465528
TSID string value: 0ARYZVZXW377R
TSID time millis since epoch value: 1670438610927
生成十个值时:
for (int i = 0; i < 10; i++) { |
我们可以看到值是单调递增的:
TSID numerical value: 388401207189971936
TSID numerical value: 388401207189971937
TSID numerical value: 388401207194165637
TSID numerical value: 388401207194165638
TSID numerical value: 388401207194165639
TSID numerical value: 388401207194165640
TSID numerical value: 388401207194165641
TSID numerical value: 388401207194165642
TSID numerical value: 388401207194165643
TSID numerical value: 388401207194165644
避免同步
因为通过TsidCreator工具提供的默认TSID工厂带有一个同步的随机值生成器,所以最好使用一个自定义的TsidFactory,提供以下优化。
- 它可以使用ThreadLocalRandom生成随机值,因此避免了同步块上的线程阻塞
- 它可以使用少量的节点位,因此为随机生成的数值留下更多的位。
因此,我们可以定义下面的TsidUtil,它为我们提供了一个TsidFactory,在我们想要生成一个新的Tsid对象时使用。
public static class TsidUtil { |
结论
使用标准 UUID 作为主键值不是一个好主意,除非第一个字节是单调递增的。
因此,使用按时间排序的 TSID 是一个更好的主意。它不仅需要标准 UUID 一半的字节数,而且更适合作为 B+Tree 索引键。
虽然 SQL Server 通过 提供按时间排序的 GUID NEWSEQUENTIALID,但 GUID 的大小为 128 位,因此它是 TSID 的两倍。
UUID 规范的第 7 版也存在同样的问题,它提供了按时间排序的 UUID。但是,它使用相同的规范格式(128 位),但格式太大了。每个引用外键列都会放大主键列存储的影响。
如果您所有的主键都是 128 位 UUID,那么主键和外键索引将需要大量空间,包括磁盘和数据库内存,因为缓冲池同时包含表和索引页。