如何使用PostgreSQL保持每行唯一的计数器?

Jul*_*ier 11 postgresql locking

我需要在 document_revisions 表中保留一个唯一的(每行)修订号,其中修订号的范围仅限于文档,因此它不是整个表唯一的,仅适用于相关文档。

我最初想出了类似的东西:

current_rev = SELECT MAX(rev) FROM document_revisions WHERE document_id = 123;
INSERT INTO document_revisions(rev) VALUES(current_rev + 1);
Run Code Online (Sandbox Code Playgroud)

但是有一个竞争条件!

我试图用 解决它pg_advisory_lock,但文档有点稀缺,我不完全理解它,我不想错误地锁定某些东西。

以下是可以接受的,还是我做错了,或者有更好的解决方案?

SELECT pg_advisory_lock(123);
current_rev = SELECT MAX(rev) FROM document_revisions WHERE document_id = 123;
INSERT INTO document_revisions(rev) VALUES(current_rev + 1);
SELECT pg_advisory_unlock(123);
Run Code Online (Sandbox Code Playgroud)

我不应该为给定的操作 (key2) 锁定文档行 (key1) 吗?所以这将是正确的解决方案:

SELECT pg_advisory_lock(id, 1) FROM documents WHERE id = 123;
current_rev = SELECT MAX(rev) FROM document_revisions WHERE document_id = 123;
INSERT INTO document_revisions(rev) VALUES(current_rev + 1);
SELECT pg_advisory_unlock(id, 1) FROM documents WHERE id = 123;
Run Code Online (Sandbox Code Playgroud)

也许我不习惯 PostgreSQL 并且 SERIAL 可以限定范围,或者可能是一个序列并且nextval()可以更好地完成工作?

Bo *_*nes 6

(我在尝试重新发现有关该主题的文章时遇到了这个问题。现在我已经找到了它,我将其发布在这里,以防其他人寻求当前所选答案的替代选项\ xe2\x80\x94windowing 与row_number())

\n\n

我有同样的用例。对于插入 SaaS 中特定项目的每条记录,我们需要一个唯一的、递增的数字,该数字可以在并发情况下生成,并且理想情况下INSERT是无缝的。

\n\n

本文描述了一个很好的解决方案,为了方便和后代,我将在这里总结一下。

\n\n
    \n
  1. 有一个单独的表作为计数器来提供下一个值。它将有两列,document_idcountercounter或者DEFAULT 0,如果您已经有一个document对所有版本进行分组的实体,则counter可以在其中添加 a 。
  2. \n
  3. 向表中添加一个BEFORE INSERT触发器document_versions,该触发器自动递增计数器 ( UPDATE document_revision_counters SET counter = counter + 1 WHERE document_id = ? RETURNING counter),然后设置NEW.version为该计数器值。
  4. \n
\n\n

或者,您可以使用 CTE 在应用程序层执行此操作(尽管出于一致性考虑,我更喜欢将其作为触发器):

\n\n
WITH version AS (\n  UPDATE document_revision_counters\n    SET counter = counter + 1 \n    WHERE document_id = 1\n    RETURNING counter\n)\n\nINSERT \n  INTO document_revisions (document_id, rev, other_data)\n  SELECT 1, version.counter, \'some other data\'\n  FROM "version";\n
Run Code Online (Sandbox Code Playgroud)\n\n

原则上,这与您最初尝试解决该问题的方式类似,只不过通过修改单个语句中的计数器行,它会阻止对过时值的读取,直到提交为止INSERT

\n\n

这是psql显示此操作的记录:

\n\n
scratch=# CREATE TABLE document_revisions (document_id integer, rev integer, other_data text, PRIMARY KEY (document_id, rev));\nCREATE TABLE\n\nscratch=# CREATE TABLE document_revision_counters (document_id integer PRIMARY KEY, counter integer DEFAULT 0);\nCREATE TABLE\n\nscratch=# WITH version AS (\n    INSERT INTO document_revision_counters (document_id) VALUES (2)\n      ON CONFLICT (document_id)\n      DO UPDATE SET counter = document_revision_counters.counter + 1\n      RETURNING counter;\n  )\n  INSERT \n    INTO document_revisions (document_id, rev, other_data)\n    SELECT 2, version.counter, \'doc 1 v1\'\n    FROM "version";\nINSERT 0 1\n\nscratch=# WITH version AS (\n    INSERT INTO document_revision_counters (document_id) VALUES (2)\n      ON CONFLICT (document_id)\n      DO UPDATE SET counter = document_revision_counters.counter + 1\n      RETURNING counter;\n  )\n  INSERT \n    INTO document_revisions (document_id, rev, other_data)\n    SELECT 2, version.counter, \'doc 1 v2\'\n    FROM "version";\nINSERT 0 1\n\nscratch=# WITH version AS (\n    INSERT INTO document_revision_counters (document_id) VALUES (2)\n      ON CONFLICT (document_id)\n      DO UPDATE SET counter = document_revision_counters.counter + 1\n      RETURNING counter;\n  )\n  INSERT \n    INTO document_revisions (document_id, rev, other_data)\n    SELECT 2, version.counter, \'doc 2 v1\'\n    FROM "version";\nINSERT 0 1\n\nscratch=# SELECT * FROM document_revisions;\n document_id | rev | other_data \n-------------+-----+------------\n           2 |   1 | doc 1 v1\n           2 |   2 | doc 1 v2\n           2 |   1 | doc 2 v1\n(3 rows)\n
Run Code Online (Sandbox Code Playgroud)\n\n

正如您所看到的,您必须小心如何INSERT发生,因此触发器版本如下所示:

\n\n
CREATE OR REPLACE FUNCTION set_doc_revision()\nRETURNS TRIGGER AS $$ BEGIN\n  WITH version AS (\n    INSERT INTO document_revision_counters (document_id, counter) VALUES (NEW.document_id, 1)\n    ON CONFLICT (document_id)\n    DO UPDATE SET counter = document_revision_counters.counter + 1\n    RETURNING counter\n  )\n\n  SELECT INTO NEW.rev counter FROM version; RETURN NEW; END;\n$$ LANGUAGE \'plpgsql\';\n\nCREATE TRIGGER set_doc_revision BEFORE INSERT ON document_revisions\nFOR EACH ROW EXECUTE PROCEDURE set_doc_revision();\n
Run Code Online (Sandbox Code Playgroud)\n\n

这使得INSERTs 更加直接,并且数据的完整性在面对INSERT来自任意来源的 s 时更加稳健:

\n\n
scratch=# INSERT INTO document_revisions (document_id, other_data) VALUES (1, \'baz\');\nINSERT 0 1\n\nscratch=# INSERT INTO document_revisions (document_id, other_data) VALUES (1, \'foo\');\nINSERT 0 1\n\nscratch=# INSERT INTO document_revisions (document_id, other_data) VALUES (1, \'bar\');\nINSERT 0 1\n\nscratch=# INSERT INTO document_revisions (document_id, other_data) VALUES (42, \'meaning of life\');\nINSERT 0 1\n\nscratch=# SELECT * FROM document_revisions;\n document_id | rev |   other_data    \n-------------+-----+-----------------\n           1 |   1 | baz\n           1 |   2 | foo\n           1 |   3 | bar\n          42 |   1 | meaning of life\n(4 rows)\n
Run Code Online (Sandbox Code Playgroud)\n


Col*_*art 4

假设您将文档的所有修订版本存储在一个表中,一种方法是不存储修订版本号,而是根据表中存储的修订版本数量来计算它。

从本质上讲,它是一个派生值,而不是您需要存储的东西。

窗口函数可用于计算修订号,例如

row_number() over (partition by document_id order by <change_date>)
Run Code Online (Sandbox Code Playgroud)

你需要一个类似的列change_date来跟踪修订的顺序。


另一方面,如果您只是revision作为文档的属性并且它指示“文档已更改了多少次”,那么我会采用乐观锁定方法,例如:

update documents
set revision = revision + 1
where document_id = <id> and revision = <old_revision>;
Run Code Online (Sandbox Code Playgroud)

如果这更新了 0 行,则说明发生了中间更新,您需要通知用户这一点。


一般来说,尝试使您的解决方案尽可能简单。在这种情况下通过

  • 除非绝对必要,否则避免使用显式锁定函数
  • 拥有较少的数据库对象(没有每个文档序列)并存储较少的属性(如果可以计算,则不存储修订版)
  • 使用单个update语句而不是select后跟insertorupdate

  • 实际上,在我的上下文中,较旧的修订版将在某个时候被删除,因此我无法计算它,否则修订版号会减少:) (3认同)