4

适合用于数据库主键的最佳UUID工具库 - Vlad Mihalcea

 1 year ago
source link: https://www.jdon.com/63768
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

适合用于数据库主键的最佳UUID工具库 - Vlad Mihalcea


在本文中,我们将了解哪种 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>
    <groupId>com.github.f4b6a3</groupId>
    <artifactId>tsid-creator</artifactId>
    <version>${tsid-creator.version}</version>
</dependency>

您可以创建一个Tsid最多可以使用 256 个节点的对象,如下所示:

Tsid tsid = TsidCreator.getTsid256();
从Tsid对象中,我们可以提取以下值:

64 位数值,
编码 64 位值的Crockford 的 Base32 字符串值,
存储在42-bit 序列中的纪元以来的 Unix 毫秒数
为了可视化这些值,我们可以将它们打印到日志中:

long tsidLong = tsid.toLong();
String tsidString = tsid.toString();
long tsidMillis = tsid.getUnixMilliseconds();
 
LOGGER.info(
    "TSID numerical value: {}",
    tsidLong
);
 
LOGGER.info(
    "TSID string value: {}",
    tsidString
);
 
LOGGER.info(
    "TSID time millis since epoch value: {}",
    tsidMillis
);

我们得到以下输出:

TSID numerical value: 388400145978465528
TSID string value: 0ARYZVZXW377R
TSID time millis since epoch value: 1670438610927

生成十个值时:

for (int i = 0; i < 10; i++) {
    LOGGER.info(
        "TSID numerical value: {}",
        TsidCreator.getTsid256().toLong()
    );
}

我们可以看到值是单调递增的:

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 {
    public static final String TSID_NODE_COUNT_PROPERTY =
        "tsid.node.count";
    public static final String TSID_NODE_COUNT_ENV =
        "TSID_NODE_COUNT";
 
    public static TsidFactory TSID_FACTORY;
 
    static {
        String nodeCountSetting = System.getProperty(
            TSID_NODE_COUNT_PROPERTY
        );
        if(nodeCountSetting == null) {
            nodeCountSetting = System.getenv(
                TSID_NODE_COUNT_ENV
            );
        }
 
        int nodeCount = nodeCountSetting != null ?
            Integer.parseInt(nodeCountSetting) :
            256;
 
        int nodeBits = (int) (Math.log(nodeCount) / Math.log(2));
 
        TSID_FACTORY = TsidFactory.builder()
            .withRandomFunction(length -> {
                final byte[] bytes = new byte[length];
                ThreadLocalRandom.current().nextBytes(bytes);
                return bytes;
            })
            .withNodeBits(nodeBits)
            .build();
    }
}

结论
使用标准 UUID 作为主键值不是一个好主意,除非第一个字节是单调递增的。
因此,使用按时间排序的 TSID 是一个更好的主意。它不仅需要标准 UUID 一半的字节数,而且更适合作为 B+Tree 索引键。
虽然 SQL Server 通过 提供按时间排序的 GUID NEWSEQUENTIALID,但 GUID 的大小为 128 位,因此它是 TSID 的两倍。

UUID 规范的第 7 版也存在同样的问题,它提供了按时间排序的 UUID。但是,它使用相同的规范格式(128 位),但格式太大了。每个引用外键列都会放大主键列存储的影响。
如果您所有的主键都是 128 位 UUID,那么主键和外键索引将需要大量空间,包括磁盘和数据库内存,因为缓冲池同时包含表和索引页。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK