PostgreSQL select 更新锁,新行

PostgreSQL select for update lock, new rows

我有以下并发用例:可以随时调用端点并且应该发生操作。在伪代码中操作是这样的(当前隔离级别是READ COMMITTED):

SELECT * FROM TABLE_A WHERE IS_LATEST=true FOR UPDATE
// DO SOME APP LOGIC TO TEST VALIDITY
// ALL GOES WELL => INSERT OR UPDATE NEW ROW WITH IS_LATEST=TRUE => COMMIT
// OTHERWISE => ROLLBACK (all good not interesting)

现在,如果这些操作中的两个在更新方面同时开始,那么 SELECT FOR UPDATE 的这种方法就可以了。因为两个事务看到相同的行数,一个将更新行,第二个事务将等待轮到它才能 SELECT FOR UPDATE 并且状态有效。

我遇到的问题是当我在第一个事务中插入时。发生的情况是,例如,当第一个事务锁定 SELECT FOR UPDATE 时,有两行,然后事务继续,在事务中间,第二个事务进来想要 SELECT FOR UPDATE(最新)并等待第一个事务完成。第一个事务完成并且数据库中实际上有一个新的 third 项目,但是第二个事务在等待时只拾取了两行行锁被释放。 (这是因为在调用 SELECT FOR UPDATE 时快照不同,只有两行匹配 IS_LATEST=true)。

有没有一种方法可以让 SELECT 锁在等待后获取最新的快照?

使用你的方法,第二个事务中的查询在锁消失后将 return 一个空结果,因为它在有问题的行上看到 is_latest = FALSE,而新行不是却可见。所以在这种情况下您将不得不重试交易。

我建议您改用 REPEATABLE READ 隔离级别和乐观锁定:

BEGIN ISOLATION LEVEL REPEATABLE READ;

SELECT * FROM table_a WHERE is_latest;  -- no locks!

/* perform your application ruminations */

UPDATE table_a SET is_latest = FALSE WHERE id = <id you found above>;

INSERT INTO table_a (is_latest, ...) VALUES (TRUE, ...);

COMMIT;

那么可能会发生三种情况:

  1. 您的查询找到一行,交易成功。

  2. 您的查询没有找到任何行,那么您可以插入第一行。

  3. 查询找到一行,但更新该行导致序列化错误。
    在那种情况下,您知道并发事务受到干扰,并且您重复完整的事务作为响应。

问题是每个命令只能看到在查询开始之前已经提交的行。有多种可能的解决方案...

更严格的隔离级别

可以使用更严格的隔离级别来解决这个问题,但这相对昂贵。

为此。

只需启动一个新命令

保持(便宜的)默认隔离级别 READ COMMITTED只需启动一个新命令

只有几行要锁定

虽然只锁定了一整行,但最简单的解决方案是重复相同的 SELECT ... FOR UPDATE。第二次迭代看到新提交的行并额外锁定它们。

理论上存在竞争条件,附加事务可能会在等待事务之前锁定新行。这将导致僵局。极不可能,但可以绝对确定,以一致的顺序锁定行:

BEGIN;  -- default READ COMMITTED

SELECT FROM table_a WHERE is_latest ORDER BY id FOR UPDATE;  -- consistent order
SELECT * FROM table_a WHERE is_latest ORDER BY id FOR UPDATE;  -- just repeat !!

--  DO SOME APP LOGIC TO TEST VALIDITY

-- pseudo-code
IF all_good
   UPDATE table_a SET is_latest = true WHERE ...;
   INSERT table_a (IS_LATEST, ...) VALUES (true, ...);
   COMMIT;
ELSE
   ROLLBACK;
END; 

(id) WHERE is_latest 上的部分索引是理想的。

更多行要锁定

为了超过一手牌,我会创建一个专用的 one-row 代币 table。 bullet-proof 实现可能如下所示,运行 作为管理员或超级用户:

CREATE TABLE public.single_task_x (just_me bool CHECK (just_me) PRIMARY KEY DEFAULT true);
INSERT INTO public.single_task_x VALUES (true);
REVOKE ALL ON public.single_task_x FROM public;
GRANT SELECT, UPDATE ON public.single_task_x TO public;  -- or just to those who need it

参见:

  • How to allow only one row for a table?

然后:

BEGIN;  -- default READ COMMITTED

SELECT FROM public.single_task_x FOR UPDATE;
SELECT * FROM table_a WHERE is_latest;  -- FOR UPDATE? ①

--  DO SOME APP LOGIC TO TEST VALIDITY

-- pseudo-code
IF all_good
   ROLLBACK;
ELSE
   UPDATE table_a SET is_latest = true WHERE ...;
   INSERT table_a (IS_LATEST, ...) VALUES (true, ...);
   COMMIT;
END; 

单把锁更便宜
① 您可能想要也可能不想额外锁定,以防御 other 写入,可能使用较弱的锁定 ....

无论哪种方式,所有锁都会在事务结束时自动释放。

咨询锁

或使用 advisory lockpg_advisory_xact_lock() 在交易期间持续存在:

BEGIN;  -- default READ COMMITTED

SELECT pg_advisory_xact_lock(123);
SELECT * FROM table_a WHERE is_latest;

-- do stuff

COMMIT;  -- or ROLLBACK; 

确保为您的特定任务使用唯一标记。 123 在我的例子中。如果您有许多不同的任务,请考虑 look-up table。

要在不同的时间点(不是事务结束时)释放锁,请考虑使用 pg_advisory_lock() 的 session-level 锁。然后您可以(并且必须)使用 pg_advisory_unlock() 手动解锁 - 或关闭会话。

这两个等待锁定资源。有 alternative functions 返回 false 而不是等待 ...