更新前的原子聚合检查

Atomic aggregate check before update

我有一个 table,其中包含许多记录、一个枚举列和一个高度多线程的环境。

CREATE TYPE status AS ENUM
    ('UNCONFIRMED', 'REGISTERED', 'VALIDATED', 'PAID');

我们的系统准确地允许 6000 validated 个状态。这个值是动态的,以后会增加但永远不会下降。

我们必须在允许更新之前检查验证记录的数量:

SELECT count(*) FROM table WHERE status='VALIDATED';

我想:

UPDATE table
   SET status='VALIDATED'
   WHERE (
        SELECT count(*)<6000 FROM table WHERE status='VALIDATED'
   ) AND id=:id;

执行此操作并保证数百个线程之间的原子性的最佳方法是什么?

我的建议:

(a) 创建一个专用的 status_counter table,对 counter 列进行约束,检查其值是否低于限制 (6000) :

CREATE TABLE status_counter (status status primary key, counter integer CHECK (counter < 6000)) ;

(b) 设置计数器:

INSERT INTO status_counter
SELECT 'VALIDATED', count(*) FROM my_table WHERE status='VALIDATED' ;

(c) 在插入或更新新行时通过 table 上的触发器更新 status_counter table :

CREATE OR REPLACE FUNCTION table_insert ()
RETURNS trigger LANGUAGE plpgsql AS
$$
BEGIN
  IF NEW.status = 'VALIDATED'
  THEN
      UPDATE status_counter
         SET counter = counter + 1
       WHERE status='VALIDATED' ;
  END IF ;
  RETURN NEW ;
END ;
$$ ;

CREATE OR REPLACE TRIGGER table_insert BEFORE INSERT ON table
FOR EACH ROW EXECUTE FUNCTION table_insert() ;

CREATE OR REPLACE FUNCTION table_update ()
RETURNS trigger LANGUAGE plpgsql AS
$$
BEGIN
  IF NEW.status IS NOT DISTINCT FROM 'VALIDATED' 
  AND OLD.status IS DISTINCT FROM 'VALIDATED'
  THEN
      UPDATE status_counter
         SET counter = counter + 1
       WHERE status='VALIDATED' ;
  ELSEIF NEW.status IS DISTINCT FROM 'VALIDATED' 
  AND OLD.status IS NOT DISTINCT FROM 'VALIDATED'
  THEN
      UPDATE status_counter
         SET counter = counter - 1
       WHERE status='VALIDATED' ;
  END IF ;
  RETURN NEW ;
END ;
$$ ;

CREATE OR REPLACE TRIGGER table_update BEFORE UPDATE OF status ON table
FOR EACH ROW EXECUTE FUNCTION table_update() ;

(d) 从现在开始,每次用户在table中插入或更新一行时,counter都会触发更新,当counter更新失败达到引发异常并阻止插入或更新行的限制。

数百个线程之间的原子性应该得到保证,因为它们都试图更新 table status_counter.

中相同的唯一行

演示在 dbfiddle.

"update where + a subquery" 是不够的,您需要将其与 table 锁、咨询锁或更高的隔离级别相结合。否则它会允许多个更新同时完成并超过计数。

select 更新”似乎是个糟糕的主意。每次尝试都必须锁定 table 中的每一行,这会产生大量流失。而且它仍然可能会遗漏一些,具体取决于是否曾经使用 status='VALIDATED'

插入行