使用node.js处理PostgreSQL中的自动ID生成并续集
#javascript #node #postgres #sequelize

数据库记录的自动ID生成是应用程序开发的基本组成部分。在本文中,我将展示四种方法来处理PostgreSQLYugabyteDBSequelize中的自动ID生成,开源,云本机,分布式SQL数据库构建了PostgreSql。

有很多方法可以处理PostgreSQL中的ID生成,但是我选择研究这些方法:

  1. 自动灌溉(序列数据类型)
  2. 序列 - 访问
  3. 使用客户端ID管理序列进行序列
  4. uuid生成

根据您的应用程序和基础数据库表,您可以选择使用其中一种或多种选项。下面我将说明如何使用Sequelize orm在node.js中实现每个。

1.自动插入

大多数开发人员在探索潜在的优化之前选择最直接的选项。我没什么不同!这是您可以在续集模型定义中创建自动插入ID字段的方法。

// Sequelize
const { DataTypes } = require('sequelize');
const Product = sequelize.define(
   "product",
   {
       id: {
           type: DataTypes.INTEGER,
           autoIncrement: true,
           primaryKey: true,
       },
       title: {
           type: DataTypes.STRING,
       }
   }
);

如果您熟悉续集,您对此语法并不陌生,但其他人可能会想知道引擎盖下实际发生了什么。

autoIncrement标志告诉PostgreSQL使用SERIAL数据类型创建id列。该数据类型隐含地创建了一个由products表的id列拥有的SEQUENCE

// PostgreSQL equivalent
CREATE SEQUENCE products_id_seq;
CREATE TABLE products
(
   id INT NOT NULL DEFAULT NEXTVAL('products_id_seq'),
   title VARCHAR(255)
);

将产品插入我们的桌子时,我们不需要为id提供值,因为它是从基础序列自动生成的。

我们可以简单地运行以下来插入产品。

// Sequelize
await Product.create({title: "iPad Pro"});


//PostgreSQL equivalent
INSERT INTO products (title) VALUES ('iPad Pro');

放下我们的桌子还将删除自动创建的序列products_id_seq

// Sequelize
await Product.drop();

// PostgreSQL equivalent
DROP TABLE products CASCADE;

尽管这种方法非常易于实现,但我们的PostgreSQL Server需要访问该序列,以便在每个写入上获得其下一个值,这是以延迟成本的形式获得的。这在分布式部署中尤为糟糕。 yugabytedb设置了一个default序列缓存大小为100。我将概述为什么在下面如此重要。

现在,我们拥有基础知识,让我们尝试加快速度。众所周知,“缓存是国王。”

2.序列辅助

尽管续集模型定义中的autoIncrement标志完全消除了直接与序列交互的需求,但在某些情况下,您可能会考虑这样做。例如,如果您想通过缓存序列值加快写入,该怎么办?不要害怕,付出一些额外的努力,我们可以实现这一目标。

如Github(https://github.com/sequelize/sequelize/issues/3555#issuecomment-1132630072)所述,

续集没有API支持来实现这一目标,但是有一个简单的解决方法。通过使用内置的literal函数,我们能够访问模型中的预定序列。

const { literal, DataTypes } = require('sequelize');
const Product = sequelize.define("product", {
 id: {
   type: DataTypes.INTEGER,
   primaryKey: true,
   defaultValue: literal("nextval('custom_sequence')"),
 },
});

sequelize.beforeSync(() => {
 await sequelize.query('CREATE SEQUENCE IF NOT EXISTS custom_sequence CACHE 50');
});

await sequelize.sync();

那还不错。因此,这就是改变的:
我们创建了自己的序列,名为custom_sequence,用于为我们的产品ID设置默认值。
此序列是在beforesync钩中创建的,因此将在产品表及其CACHE值设置为50之前创建。
defaultValue设置为我们自定义序列中的下一个值。

好吧,缓存呢? PostgreSQL中的序列可以选择在创建时提供一个CACHE值,该值分配了要存储在每个会话中的内存中的一定数量的值。将我们的缓存设置为50,这是其工作原理。

//Database Session A
> nextval('custom_sequence');
1
> nextval('custom_sequence');
2

//Database Session B
> nextval('custom_sequence');
51
>
52

对于具有多个数据库连接的应用程序,例如一个运行的微服务或负载平衡器后面的多个服务器,每个连接将接收一组缓存值。不会在其缓存中包含重复值,从而确保插入记录时没有碰撞。实际上,根据数据库的配置方式,您甚至可以在测序的id列中找到gaps,如果数据库连接失败并重新启动而不使用其缓存中分配的所有值。但是,这通常不是问题,因为我们只关心独特性。

那么,有什么意义?速度。速度是重点!

通过在我们的PostgreSQL后端缓存值并将其存储在内存中,我们可以很快地检索下一个值。实际上,yugabytedb caches 100 sequence values by default,而不是1的后QL默认值1。这允许数据库扩展,而无需重复从写入的主节点从字母节点上获得下一个序列值。当然,缓存带来了在PostgreSQL Server上增加内存约束的缺点。

根据您的基础架构,这可能是值得的优化!

3.客户端测序

序列 - 通过缓存值在我们的后QL后端中提高性能。我们如何使用序列来缓存我们的客户端?

PostgreSQL中的序列具有一个称为INCREMENT BY的附加参数,可用于实现此目的。

// DB Initialization
const { literal, DataTypes } = require('sequelize');
const Product = sequelize.define("product", {
 id: {
   type: DataTypes.INTEGER,
   primaryKey: true
 },
});

sequelize.beforeSync(() => {
 await sequelize.query('CREATE SEQUENCE IF NOT EXISTS custom_sequence INCREMENT BY 50');
});

await sequelize.sync();

// Caller
const startVal = await sequelize.query("nextval('custom_sequence')");
const limit = startVal + 50;

if (startVal >= limit) {
   const startVal = await sequelize.query("nextval('custom_sequence')");
   const limit = startVal + 50;
}

await Product.create({id: startVal, title: "iPad Pro"})
startVal += 1;

在这里,我们以略有不同的方式利用我们的自定义序列。我们的模型定义不提供默认值。取而代之的是,我们使用此序列来设置唯一值客户端,通过遍历增量范围内的值。当我们用尽了此范围内的所有值时,我们会再次呼吁我们的数据库,以使我们的序列中的下一个值以“刷新”我们的范围。

这是一个示例:

// Database Session A

> nextval('custom_sequence');
1

*
 inserts 50 records
 // id 1
 // id 2
 ...
 // id 50
*

> nextval('custom_sequence');
151

// Database Session B

> nextval('custom_sequence');
51

* inserts 50 records before Session A has used all numbers in its range *

> nextval('custom_sequence');
101

数据库会话A 连接并接收序列中的第一个值。 数据库会话B 连接并接收51的值,因为我们将INCREMENT BY值设置为50。像我们的自动报重解决方案一样,我们可以通过引用我们的PostgreSQL序列来确定我们的范围的起始值。

此解决方案可能会出现哪些问题?好吧,数据库管理员可能会选择增加或降低特定序列的INCREMENT BY值,而无需通知应用程序开发人员此更改。这将破坏应用程序逻辑。

我们如何从客户端测序中受益?如果您在应用程序服务器节点上有很多可用的内存,那么这可能是与数据库节点上的semence-caching相比的潜在性能好处。

实际上,您可能想知道是否可以在同一实现中使用客户端和服务器上的缓存。简短的答案是肯定的。通过使用CACHEINCREMENT BY值创建序列,我们从序列值的服务器端缓存和客户端缓存中受益于我们范围内的下一个值。如果内存约束不是主要问题,则这种性能优化提供了两者中最好的。

已经足够了!让我们继续使用唯一的标识符。

4. UUID生成

我们涵盖了生成基于整数的ID的三种方法。另一种数据类型是普遍唯一的标识符(UUID),完全消除了对序列的需求。

a uuid是一个128位标识符,它具有独特性的保证,这是由于非常小的概率将同一ID产生两次。

PostgreSQL带有一个称为pgcrypto的扩展名(也由YugabytedB支持),可以安装使用gen_random_uuid函数来生成UUID。此函数为数据库列生成了一个UUID值,与序列一起使用nextval几乎相同。

此外,node.js有几个软件包生成uuids,例如,您猜对了,uuid

// Sequelize
const { literal, DataTypes } = require('sequelize');
const Product = sequelize.define(
   "product",
   {
       id: {
           type: DataTypes.UUID,
           defaultValue: literal('gen_random_uuid()')
           primaryKey: true,
       },
       title: {
           type: DataTypes.STRING,
       }
   }
);

sequelize.beforeSync(() => {
 await sequelize.query('CREATE EXTENSION IF NOT EXISTS "pgcrypto"');
});


// PostreSQL Equivalent
CREATE TABLE products
(
   id UUID NOT NULL DEFAULT gen_random_uuid(),
   title VARCHAR(255)
);

这使我们能够在需要的情况下以服务器端默认生成UUID客户端。

一种基于UUID的方法带来了独特的好处,数据类型的随机性质有助于某些数据迁移。这对API安全也有帮助,因为唯一标识符与存储的信息无关。

此外,在不管理状态的情况下生成ID客户端的能力对分布式部署有所帮助,在分布式部署中,网络潜伏在应用程序性能中起着重要作用。

例如,在geo-partioned yugabytedb群集中,与最近的数据库节点建立连接以提供低延迟读取。但是,按照写作,该节点必须将请求转发到群集中的主节点(可以驻留在世界另一个区域)中,以确定下一个序列值。 UUID的使用消除了此流量,提供了性能提升。

那么,缺点是什么?好吧,uuids的话题有些是polarizing。一个明显的缺点是相对于整数的uuid的存储尺寸,16个字节,而不是4个字节,而INT的4个字节和BIGINT的4个字节。 UUID还需要一些时间来生成,这是一个性能考虑。

在此post中说明了有关使用UUID作为主要密钥的一些问题,并在此thread中就Yugabytedb进行了进一步讨论。

您可以阅读有关整数和基于UUID ID之间的权衡的更多信息here

建造

最终,选择如何生成数据库ID时,有许多因素需要考虑。您可能会选择使用自动插入ID作为不经常写作的表格,或者不需要低延迟写入的表。另一表分布在多节点部署中的多个地理位置上,可能会受益于使用UUID。只有一种方法可以找出答案。出去写一些代码!

如果您有兴趣使用始终免费的,postgresql兼容的数据库节点,请尝试YugabyteDB Managed尝试。