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


10

我需要在document_revisions表中保留一个唯一的(每行)修订号,该修订号的作用域是一个文档,因此它不是整个表唯一的,而仅是相关文档的唯一。

我最初想到的是:

current_rev = SELECT MAX(rev) FROM document_revisions WHERE document_id = 123;
INSERT INTO document_revisions(rev) VALUES(current_rev + 1);

但是有比赛条件!

我正在尝试使用来解决它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);

我是否应该为给定操作(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;

也许我不习惯PostgreSQL,可以对SERIAL进行范围划分,或者确定序列范围,并且nextval()会做得更好?


我不明白您对“给定操作”的含义以及“ key2”的来源。
TrygveLaugstøl13年

2
如果您要进行悲观锁定,则锁定策略看起来还可以,但是我会使用pg_advisory_xact_lock,因此所有锁定都会在COMMIT / ROLLBACK上自动释放。
TrygveLaugstøl13年

Answers:


2

假设您将文档的所有修订存储在一个表中,则一种方法是存储修订号,而是根据表中存储的修订数进行计算。

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

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

row_number() over (partition by document_id order by <change_date>)

并且您将需要一列change_date来跟踪修订的顺序。


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

update documents
set revision = revision + 1
where document_id = <id> and revision = <old_revision>;

如果此更新为0行,则表明存在中间更新,您需要将此通知用户。


通常,请尝试使解决方案尽可能简单。在这种情况下

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

确实,我不需要在可以计算时存储值。谢谢你的提醒!
朱利安·波特尼尔

2
实际上,在我看来,较旧的修订版本会在某个时候删除,因此我无法计算出来,否则修订版本数量会减少:)
Julien Portalier

3

SEQUENCE被保证是唯一的,并且如果您的文档数量不是太高(否则您需要管理很多序列),您的用例就显得适用。使用RETURNING子句获取由序列生成的值。例如,使用“ A36”作为document_id:

  • 对于每个文档,您可以创建一个序列来跟踪增量。
  • 管理序列需要谨慎处理。您也许可以保留一个单独的表,其中包含文档名称以及与之相关的序列,document_id以便在插入/更新该document_revisions表时进行引用。

     CREATE SEQUENCE d_r_document_a36_seq;
    
     INSERT INTO document_revisions (document_id, rev)
     VALUES ('A36',nextval('d_r_document_a36_seq')) RETURNING rev;
    

感谢格式化deszo,我没有注意到粘贴评论时看起来有多糟。
bma 2013年

如果您希望下一个值是前一个+ 1,因为它们不在事务中运行,那么序列是一个不好的计数器。
TrygveLaugstøl13年

1
h?序列是原子的。这就是为什么我建议文档顺序。它们也不能保证没有间隙,因为回滚在序列增加后不会减小。我并不是说适当的锁定不是一个好的解决方案,只是序列提供了一种替代方案。
bma

1
谢谢!如果我需要存储修订号,序列肯定是必经之路。
朱利安·波塔尼尔

2
请注意,拥有大量的序列是性能的主要问题,因为序列本质上是一个具有一行的表。您可以在此处
Magnuss

2

这通常可以通过乐观锁定来解决:

SELECT version, x FROM foo;

version | foo
    123 | ..

UPDATE foo SET x=?, version=124 WHERE version=123

如果更新返回0行已更新,则您错过了更新,因为其他人已经更新了该行。


谢谢!当您需要保留文档更新计数器时,这是一个很好的选择!但是我为document_revisions表中的每一行都需要一个唯一的修订版本号,该版本号不会被更新,并且必须是先前修订版本的关注者(即,上一行的修订版本号+ 1)。
Julien Portalier

1
嗯,那你为什么不使用这种技术呢?这是唯一使您无间隙的序列的方法(悲观锁定除外)。
TrygveLaugstøl13年

2

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

我有相同的用例。对于插入到SaaS中特定项目中的每条记录,我们需要一个唯一的递增编号,该编号可以在并发INSERTs的情况生成,并且理想情况下是无间隙的。

本文介绍了一个不错的解决方案,为方便起见,我将在此总结。

  1. 有一个单独的表作为提供下一个值的计数器。它将有两列,document_idcountercounterDEFAULT 0另外,如果你已经有了一个document组织了所有版本,一个实体counter可以有加。
  2. BEFORE INSERT向该document_versions表添加一个触发器,该触发器可以自动使计数器(UPDATE document_revision_counters SET counter = counter + 1 WHERE document_id = ? RETURNING counter)递增,然后将其设置NEW.version为该计数器值。

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

WITH version AS (
  UPDATE document_revision_counters
    SET counter = counter + 1 
    WHERE document_id = 1
    RETURNING counter
)

INSERT 
  INTO document_revisions (document_id, rev, other_data)
  SELECT 1, version.counter, 'some other data'
  FROM "version";

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

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

scratch=# CREATE TABLE document_revisions (document_id integer, rev integer, other_data text, PRIMARY KEY (document_id, rev));
CREATE TABLE

scratch=# CREATE TABLE document_revision_counters (document_id integer PRIMARY KEY, counter integer DEFAULT 0);
CREATE TABLE

scratch=# WITH version AS (
    INSERT INTO document_revision_counters (document_id) VALUES (2)
      ON CONFLICT (document_id)
      DO UPDATE SET counter = document_revision_counters.counter + 1
      RETURNING counter;
  )
  INSERT 
    INTO document_revisions (document_id, rev, other_data)
    SELECT 2, version.counter, 'doc 1 v1'
    FROM "version";
INSERT 0 1

scratch=# WITH version AS (
    INSERT INTO document_revision_counters (document_id) VALUES (2)
      ON CONFLICT (document_id)
      DO UPDATE SET counter = document_revision_counters.counter + 1
      RETURNING counter;
  )
  INSERT 
    INTO document_revisions (document_id, rev, other_data)
    SELECT 2, version.counter, 'doc 1 v2'
    FROM "version";
INSERT 0 1

scratch=# WITH version AS (
    INSERT INTO document_revision_counters (document_id) VALUES (2)
      ON CONFLICT (document_id)
      DO UPDATE SET counter = document_revision_counters.counter + 1
      RETURNING counter;
  )
  INSERT 
    INTO document_revisions (document_id, rev, other_data)
    SELECT 2, version.counter, 'doc 2 v1'
    FROM "version";
INSERT 0 1

scratch=# SELECT * FROM document_revisions;
 document_id | rev | other_data 
-------------+-----+------------
           2 |   1 | doc 1 v1
           2 |   2 | doc 1 v2
           2 |   1 | doc 2 v1
(3 rows)

如您所见,您必须注意INSERTs的发生方式,因此要注意触发器版本,如下所示:

CREATE OR REPLACE FUNCTION set_doc_revision()
RETURNS TRIGGER AS $$ BEGIN
  WITH version AS (
    INSERT INTO document_revision_counters (document_id, counter) VALUES (NEW.document_id, 1)
    ON CONFLICT (document_id)
    DO UPDATE SET counter = document_revision_counters.counter + 1
    RETURNING counter
  )

  SELECT INTO NEW.rev counter FROM version; RETURN NEW; END;
$$ LANGUAGE 'plpgsql';

CREATE TRIGGER set_doc_revision BEFORE INSERT ON document_revisions
FOR EACH ROW EXECUTE PROCEDURE set_doc_revision();

INSERT面对INSERT来自任意来源的,这将使s更加简单明了,数据的完整性也更加可靠:

scratch=# INSERT INTO document_revisions (document_id, other_data) VALUES (1, 'baz');
INSERT 0 1

scratch=# INSERT INTO document_revisions (document_id, other_data) VALUES (1, 'foo');
INSERT 0 1

scratch=# INSERT INTO document_revisions (document_id, other_data) VALUES (1, 'bar');
INSERT 0 1

scratch=# INSERT INTO document_revisions (document_id, other_data) VALUES (42, 'meaning of life');
INSERT 0 1

scratch=# SELECT * FROM document_revisions;
 document_id | rev |   other_data    
-------------+-----+-----------------
           1 |   1 | baz
           1 |   2 | foo
           1 |   3 | bar
          42 |   1 | meaning of life
(4 rows)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.