后gresql的可扩展序列
#postgres #sql #database #yugabytedb

可伸缩性的最佳实践是避免序列,并使用主键的UUID。但是,SQL序列有一些充分的理由:它们较小,保证唯一性(不是很高的概率,而是真正的数学保证),它们将插入在一起的行聚集在一起,这对B树索引有益于B-Tree索引,也许对于其他查询。我在previous post中解释说,由于缓存,序列可以扩展。

然而, postgresql缓存了每个连接的序列(与实例共享内存中缓存的oracle不同)。在连接风暴中,所有序列都可以是一个瓶颈,在该风暴中都有一个冷缓存,这是解决方法。请注意,由于许多其他原因,在PostgreSQL中重新连接很昂贵,并且更好地解决了不太动态的连接池(如果您需要动态的话,应该看看FlexiPool

NextVal()超载

PostgreSQL nextval()函数将regclass作为输入。如果提供了文本,则将其铸造为带有此名称的序列的重级。

我将扩展此功能,并以一个名称为文本并查找特定后缀,并带有%,然后是一个数字。

  • 如果没有这样的后缀,则使用name::regclass调用Postgres函数。
  • 如果有这样的后缀,我将%[0-9]+零件替换为一个随机数模拟%之后传递的值。

例如,如果我打电话给seq%4,它将随机从seq0seq1seq2seq3中获取下一个值。当然,必须事先创建它们。

想法是,当我声明类似的内容时:

create table demo ( id bigint default nextval('seq%4') primary key)

其中“ seq%4”不是序列,它将从创建的4个序列中读取,从seq0seq3

我的目标是分发有连接风暴(所有带有冷缓存)时读取的序列,而不是随机数,我使用pg_backed_pid(),以便相同的会话使用相同的序列,但不同的会话将避免使用热点。

这是功能:

create function nextval(name text) returns bigint as $$
declare
 prefix text;
 buckets int;
 sequence regclass;
begin
 prefix :=   regexp_replace(name,'^(.*)%([0-9]+)$','\1');
 if prefix = name then
  sequence:=name::regclass;
 else
  buckets := regexp_replace(name,'^(.*)%([0-9]+)$','\2');
  sequence:=(prefix||(pg_backend_pid()%buckets)::text)::regclass;
 end if;
 return pg_catalog.nextval(sequence);
end;
$$ language plpgsql;

多个交织的序列

在此演示中,我将扩展到8个序列。我创建了以seq为前缀的8个序列,并添加一个从0到7的数字。这是我用select nextval('seq%8')调用我的功能的数字,因为它将用Modulo 8替换后缀。

因为目标是具有唯一的数字,所以每个序列必须从不同的数字开始,从1到8开始,然后将其从8个开始,并且会增加8个。将缓存所有8的倍数,从0到792。seq1将具有1到793的数字,依此类推。


yugabyte=#

select format(
'create sequence if not exists %I minvalue %s start with %s cache 100 increment by %s'
,'seq'||n,n,n,1+max(n)over()) as "CREATE SEQUENCE"
from generate_series(0,7) n;
\gexec

                                   CREATE SEQUENCE
-------------------------------------------------------------------------------------
 create sequence if not exists seq0 minvalue 0 start with 0 cache 100 increment by 8
 create sequence if not exists seq1 minvalue 1 start with 1 cache 100 increment by 8
 create sequence if not exists seq2 minvalue 2 start with 2 cache 100 increment by 8
 create sequence if not exists seq3 minvalue 3 start with 3 cache 100 increment by 8
 create sequence if not exists seq4 minvalue 4 start with 4 cache 100 increment by 8
 create sequence if not exists seq5 minvalue 5 start with 5 cache 100 increment by 8
 create sequence if not exists seq6 minvalue 6 start with 6 cache 100 increment by 8
 create sequence if not exists seq7 minvalue 7 start with 7 cache 100 increment by 8
(8 rows)


yugabyte=# \gexec
CREATE SEQUENCE
CREATE SEQUENCE
CREATE SEQUENCE
CREATE SEQUENCE
CREATE SEQUENCE
CREATE SEQUENCE
CREATE SEQUENCE
CREATE SEQUENCE
yugabyte=#

测试单个序列

当然,当我超载Postgres功能时,我必须保证使用text调用时相同的行为:

yugabyte=# select nextval('seq1'),* from seq1;
 nextval | last_value | log_cnt | is_called
---------+------------+---------+-----------
       1 |          1 |       0 | f
(1 row)

yugabyte=# select nextval('seq1'),* from seq1;
 nextval | last_value | log_cnt | is_called
---------+------------+---------+-----------
       9 |        793 |       0 | t
(1 row)

yugabyte=# \connect
psql (13.7, server 11.2-YB-2.17.0.0-b0)
You are now connected to database "yugabyte" as user "yugabyte".
yugabyte=# select nextval('seq1'),* from seq1;
 nextval | last_value | log_cnt | is_called
---------+------------+---------+-----------
     801 |        793 |       0 | t
(1 row)

yugabyte=# select nextval('seq1'),* from seq1;
 nextval | last_value | log_cnt | is_called
---------+------------+---------+-----------
     809 |       1593 |       0 | t
(1 row)

这是postgresql中的正常行为:缓存是每条时间。如果我重新连接,则获取新值以加热缓存。您可以看到,在查询序列本身时:对nextval()的第一个调用返回1(start value) and updated the sequence to 793 (to skipcache*Increment by by)。同一会话中的下一个呼叫获取那99个数字,而无需访问序列。另一个连接从793之后的下一个连接开始,并将序列更新为1593。

请注意,如果我使用regclass调用该函数,它将直接调用Postgres函数。如果您具有%'+number前缀的真实序列,则可以使用它。或者您可以更改前缀。我用“%”看起来像模型。

但是,用increment by 8创建的序列的目标是用seq%8调用我的功能。

测试可扩展序列

现在我将使用我的特殊模式nextval('seq%8')调用该功能:

yugabyte=# select nextval('seq%8');
 nextval
---------
       2
(1 row)

yugabyte=# \connect
psql (13.7, server 11.2-YB-2.17.0.0-b0)
You are now connected to database "yugabyte" as user "yugabyte".
yugabyte=# select nextval('seq%8');
 nextval
---------
       5
(1 row)

yugabyte=# \connect
psql (13.7, server 11.2-YB-2.17.0.0-b0)
You are now connected to database "yugabyte" as user "yugabyte".
yugabyte=# select nextval('seq%8');
 nextval
---------
     805
(1 row)

yugabyte=# \connect
psql (13.7, server 11.2-YB-2.17.0.0-b0)
You are now connected to database "yugabyte" as user "yugabyte".
yugabyte=# select nextval('seq%8');
 nextval
---------
       0
(1 row)

您可以从拾取的数字中看到,每个新连接都从另一个序列读取,除了pid具有相同模型8的第三个序列,然后使用冷cache读取。

独特而最小的差距

我想确保我没有重复,并且在同一会话中插入时也没有差距。

i用default nextval('seq%8')创建一个表作为id的默认值,并从许多会话中插入1000行。

yugabyte=# create table demo (
            id bigint default nextval('seq%8') primary key
            , n int
);
CREATE TABLE

yugabyte=# insert into demo(n) select generate_series(1,1000);
INSERT 0 1000
yugabyte=# \connect
psql (13.7, server 11.2-YB-2.17.0.0-b0)
You are now connected to database "yugabyte" as user "yugabyte".

yugabyte=# insert into demo(n) select generate_series(1,1000);
INSERT 0 1000
yugabyte=# \connect
psql (13.7, server 11.2-YB-2.17.0.0-b0)
You are now connected to database "yugabyte" as user "yugabyte".

...

我已经从多次会话中运行这些插入/重新连接。没有重复的关键错误,这里有一系列ID:

Image description

这看起来像是用increment by 1生成的一个序​​列生成的,但是在需要缓存另一个范围时可以分配对多个序列的访问的优势。

为什么以及何时呢?

缓存的序列可能足以在精心设计的应用程序(使用连接池)中可伸缩。在100个缓存的情况下,该序列将被读取,并且每100个调用一次。但是,如果您的连接池太动态了,并且同时启动数百个连接,它们将同时访问序列。

使用此功能的另一个原因是当您不使用缓存序列(用cache 1定义,这是PostgreSQL默认值)。但是,同样,这可能不是最好的设计。缓存序列具有另一个优势:并发会话正在在不同范围内生成ID,这可能会减少支持主键的B树上的某些热点。

在yugabytedb中,您没有这个热点问题,因为您可能会在ID上进行散布。但是,从多个序列中读取可能是缓存的不错的补充,尤其是在地理分布的簇中,延迟在连接风暴的情况下增加了争论。

请注意,这与Oracle数据库Scalable Sequences不同,这更像是Partitioned Sequence,其目标是避免B-Tree热点。通过我的功能,数字与increment by交织在一起以保持数字低。