小心使用UUID, PostgreSQL中的UUID的弊端及解决方案

1、背景:

PostgreSQL中,确实提供了对UUID的支持。但是用在在实际的生产系统中,因为uuid天然处于无序状态,它又被大量用作主键用于支持全球的分布式ID,因为它处于无序状态,这给底层的B-树索引的CUD带来很大的性能问题。

因为前后插入的数据基本上大部分时候都不在同一个index页上,这会经常引起索引页的分裂,更新操作也是如此。另外存储空间也是一个问题。毕竟多达36或32个字节的长度放在那里。无序导致频繁分裂,如果理解B-树的基本原理,应该比较容易理解。(德哥有很多篇分析UUID类型的相关性能问题,可以自行搜索)

我们用一张表20万记录来简单比较下它们的大小:

mydb=# create table t(id bigserial primary key, col2 varchar(32));
CREATE TABLE
mydb=# insert into t select n, 'test' ||n from generate_series(1, 200000) as n;
INSERT 0 200000

mydb=# select pg_size_pretty(pg_total_relation_size('t'));
 pg_size_pretty
----------------
 14 MB
(1 row)

mydb=# create table t2(id varchar(36) primary key, col2 varchar(32));
CREATE TABLE

mydb=# insert into t2 select ''||gen_random_uuid(), 'test' ||n from generate_series(1, 200000) as n;
INSERT 0 200000
mydb=# select pg_size_pretty(pg_total_relation_size('t2'));
 pg_size_pretty
----------------
 30 MB
(1 row)
                                                             ^
mydb=# select pg_size_pretty(pg_indexes_size('t')) as t1_idx_size, pg_size_pretty(pg_indexes_size('t2')) as t2_idx_size;
 t1_idx_size | t2_idx_size
-------------+-------------
 4408 kB     | 15 MB
(1 row)

索引空间的大小,uuid是bigint的近4倍。这只是其中的一方面。最重要的,它的无序状态会带来很多别的问题。在生产环境中,要得到相对较好的性能,我们建议尽量不用或者采用其它方式。

在仍然要使用uuid或类似uuid的情况下(大部分情况下, 使用序列基本上能满足需求),可以有如下方案:

2、解决方案

2.1、添加一个timestamp前缀到UUID

我们主动在uuid的前方添加一个以秒为单位的timestamp前缀,确保它有序,并且也是全局唯一的。因为后边的uuid本身是唯一的。这样以少量的冗余存储,取得比较好的性能,同时也保证了唯一性。

mydb=#  create table t3(id varchar(46) primary key, col2 varchar(32), crt_time timestamp);
CREATE TABLE
mydb=#  insert into t3 values(extract(epoch from now()::timestamp(0))::varchar(10)||gen_random_uuid(), 'test', now());
INSERT 0 1
mydb=#  insert into t3 values(extract(epoch from now()::timestamp(0))::varchar(10)||gen_random_uuid(), 'test', now());
INSERT 0 1
mydb=# select * from t3;
                       id                       | col2 |          crt_time
------------------------------------------------+------+----------------------------
 1681155021b9eae95c-3832-4ddd-8f73-29073b3c1aeb | test | 2023-04-10 19:30:20.645108
 1681155024f2e7c64c-58e8-4223-9023-b70585200d18 | test | 2023-04-10 19:30:23.829709
(2 rows)

如果不截掉"-",可以用46个字节的宽度来描述,去掉后,可以用42个字节。

2.2、仍然有timestamp前缀,但是我们使用NanoID

事实上,我们如果使用NanoID, 可以达到非常好的性能,存储空间上也远小于UUID。它一般只占21个字节的宽度,生成的速度还快于UUID。宽度还可以定制。

基于上边的思路,我们可以很快构建出一个+NanoID,这样既可以全局唯一,即算是在客户端生成,也可以保证“基本”有序。为什么是基本,因为这个ID的组合是在DB的客户端生成的。但影响基本上可以忽略。至于这个NanoID,我们在很多场合能看到它的影子,如下例,一个公众号文章的链接地址:

https://mp.weixin.qq.com/s/pEiuiwavWrsZTRoDQuZQ6A

pEiuiwavWrsZTRoDQuZQ6A 共计22个字符。大概率就是NanoID生成的。它还可以通过定制字母表来生成符合规则的一些密码,比UUID更灵活。使用即将介绍的插件:pg_nanoid, 我们可以瞬间造出大量符合上述条件的"id"串,如下:

mydb=# select gen_nanoid(22, '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') as weixin_digest_id from generate_series(1, 10);
    weixin_digest_id
------------------------
 3dkWFCEk8wNGTxKdC1vhaF
 q0KVvvSPLJJSNMDXMJFTBc
 07WuQVvFBmQJry9JnZHs9v
 6nI68G7afLhFQ3bWg9gkvb
 AhF171QC1TrYPBlIZ6wWqR
 TGFsLgitmjMDiIgRp6fOqw
 Dw6jjYXr8vBVhgPC3gwJmg
 BH0M4grAEemx9VBEdtyBLP
 r47RimNILY1o6RpOvsyZEV
 mmZ6Die6cpXkme1JL3PLQf
(10 rows)

思路在这里,实现起来也不麻烦。同时可以避免无序带来检索以及索引CUD方面的低效和库表空间的快速肿胀。

PostgreSQL中,自身也有已经实现的plugin:  https://github.com/jaiminpan/pg_nanoid 这样,在服务器端生成,一样可以跟UUID正面PK。

我们可以很方便的使用32个字节来描述局部有序的NanoID:

mydb=# create extension pg_nanoid;
CREATE EXTENSION
mydb=# create table t5(id varchar(32) primary key, col2 text);
CREATE TABLE
mydb=# insert into t5 select extract(epoch from now()::timestamp(0))::varchar(10)||gen_nanoid(22), 'test' from generate_series(1, 10);
INSERT 0 10
mydb=# select * from t5;
                id                | col2
----------------------------------+------
 1681159321KJTbh4Ac6TI31yeOwEtuR6 | test
 16811593211m3REwV1ZlOCjEhzlgwzG_ | test
 1681159321G_BN9casKZQPnSujM95xUL | test
 1681159321kgEuNTjykQvxut-ApRJyRg | test
 1681159321CfxA5RFetFOsdLRy2rZZb7 | test
 16811593215rjFmhM4S1Y0ASs42RACpl | test
 1681159321Shp9Skpv9pOHJcQQ3QmGMt | test
 1681159321zvQTYvHVBOKceL67Ik_Yud | test
 1681159321QfCH9ZjwEptcxhJ87HBwYl | test
 1681159321PYs8NFkDUMAkcv5iM4B-uv | test
(10 rows)

mydb=# select * from t5 order by id;
                id                | col2
----------------------------------+------
 16811593211m3REwV1ZlOCjEhzlgwzG_ | test
 16811593215rjFmhM4S1Y0ASs42RACpl | test
 1681159321CfxA5RFetFOsdLRy2rZZb7 | test
 1681159321G_BN9casKZQPnSujM95xUL | test
 1681159321kgEuNTjykQvxut-ApRJyRg | test
 1681159321KJTbh4Ac6TI31yeOwEtuR6 | test
 1681159321PYs8NFkDUMAkcv5iM4B-uv | test
 1681159321QfCH9ZjwEptcxhJ87HBwYl | test
 1681159321Shp9Skpv9pOHJcQQ3QmGMt | test
 1681159321zvQTYvHVBOKceL67Ik_Yud | test
(10 rows)

2.3、使用有序UUID

用到extension:  http://github.com/tvondra/sequential-uuids

postgres=# create extension sequential_uuids;
CREATE EXTENSION

postgres=# \dx+ sequential_uuids
         Objects in extension "sequential_uuids"
                    Object description
----------------------------------------------------------
 function uuid_sequence_nextval(regclass,integer,integer)
 function uuid_time_nextval(integer,integer)
(2 rows)

postgres=# create table t4(id uuid primary key, col2 text);
CREATE TABLE
postgres=# insert into t4 select uuid_time_nextval(), 'test' || n from generate_series(1, 10) as n;
INSERT 0 10
postgres=# select * from t4 order by id;
                  id                  |  col2
--------------------------------------+--------
 8a4e43da-9958-4aba-b641-3f42e7c9a68a | test7
 8a4e4c66-edc1-4a69-9f09-5b18f3bc2bfb | test3
 8a4e514d-cda1-4402-be60-3cc678e16a29 | test6
 8a4e6055-9b44-4ef1-80dc-583364d5442c | test8
 8a4e7210-6a2e-489c-876b-b44310670d10 | test1
 8a4e9ad0-305f-4c63-9256-123face693c9 | test10
 8a4e9e80-6863-489e-a6e9-b6c004c56919 | test2
 8a4ebc76-39ee-4d6f-9c7a-b94c1c0ef94c | test5
 8a4ec237-bce5-474e-b0d1-00e1dd6d1b27 | test4
 8a4ec39d-5d59-4ece-a6a4-13dca90bf03e | test9
(10 rows)

postgres=# select * from t4;
                  id                  |  col2
--------------------------------------+--------
 8a4e7210-6a2e-489c-876b-b44310670d10 | test1
 8a4e9e80-6863-489e-a6e9-b6c004c56919 | test2
 8a4e4c66-edc1-4a69-9f09-5b18f3bc2bfb | test3
 8a4ec237-bce5-474e-b0d1-00e1dd6d1b27 | test4
 8a4ebc76-39ee-4d6f-9c7a-b94c1c0ef94c | test5
 8a4e514d-cda1-4402-be60-3cc678e16a29 | test6
 8a4e43da-9958-4aba-b641-3f42e7c9a68a | test7
 8a4e6055-9b44-4ef1-80dc-583364d5442c | test8
 8a4ec39d-5d59-4ece-a6a4-13dca90bf03e | test9
 8a4e9ad0-305f-4c63-9256-123face693c9 | test10
(10 rows)

postgres=# create table t5(id uuid primary key, col2 text);
CREATE TABLE
postgres=# create sequence t5_seq;
CREATE SEQUENCE
postgres=# insert into t5 select uuid_sequence_nextval('t5_seq'),  'test' || n from generate_series(1, 10) as n;
INSERT 0 10

postgres=# select * from t5 order by id;
                  id                  |  col2
--------------------------------------+--------
 0000034d-cb19-4f0a-977a-9d60c120d71e | test1
 00004a8d-cac7-4d80-8a87-a546dcff4cee | test7
 00004e72-a707-4c18-b39f-6565f5fb1201 | test3
 000051b2-bcba-4916-ab00-ecf1ac4438a1 | test8
 000079cb-3803-4964-94db-29d9ce0a3c63 | test5
 00008414-de3c-4a4b-adac-e0ba6c10546c | test10
 00009ae4-23a1-4c2b-8c32-a50380e58262 | test4
 0000c80e-998c-499f-96be-d33fc545b07b | test9
 0000d0a8-c178-4cfe-af59-82089cecd6f4 | test6
 0000de4f-c050-43d6-a343-e0ce1e6bacba | test2
(10 rows)

postgres=# select * from t5;
                  id                  |  col2
--------------------------------------+--------
 0000034d-cb19-4f0a-977a-9d60c120d71e | test1
 0000de4f-c050-43d6-a343-e0ce1e6bacba | test2
 00004e72-a707-4c18-b39f-6565f5fb1201 | test3
 00009ae4-23a1-4c2b-8c32-a50380e58262 | test4
 000079cb-3803-4964-94db-29d9ce0a3c63 | test5
 0000d0a8-c178-4cfe-af59-82089cecd6f4 | test6
 00004a8d-cac7-4d80-8a87-a546dcff4cee | test7
 000051b2-bcba-4916-ab00-ecf1ac4438a1 | test8
 0000c80e-998c-499f-96be-d33fc545b07b | test9
 00008414-de3c-4a4b-adac-e0ba6c10546c | test10
(10 rows)

有人也许会问,这也不是完全有序的啊。是的,它并不是完全有序,而是局部有序,就是做到这种局部有序,已经可以大大的改善相关性能。

小结:

其实,关于分布式ID,还有很多别的方案,比如雪花ID之类的。上述各种方案都能缓解或解决UUID使用导致的性能和维护问题。采用“有序”的NanoID (既能控制长度,又能保证有序),应该是一种非常好的解决方案。我也极力推荐这种方案。客户端编码也非常简单。值得一提的是,NanoID的性能比UUID要好很多,存储要求也低。即算不考虑有序无序,它也是一种更好的替代方案。

参考:

1、Sequential UUID Generators by https://www.2ndquadrant.com
https://www.2ndquadrant.com/en/blog/sequential-uuid-generators

2、jNanoid
https://github.com/aventrix/jnanoid

3、pg_nanoid:  https://github.com/jaiminpan/pg_nanoid


请使用浏览器的分享功能分享到微信等