postgresql 原子更新.. Postgres中的SELECT

mutmk8jj  于 2023-02-15  发布在  PostgreSQL
关注(0)|答案(2)|浏览(171)

我正在构建一个排序的队列机制,有需要处理的数据行和一个状态标志,我使用update .. returning子句来管理它:

UPDATE stuff
SET computed = 'working'
WHERE id = (SELECT id from STUFF WHERE computed IS NULL LIMIT 1)
RETURNING *

嵌套的选择部分是否与更新的锁相同,或者我在这里有一个争用条件?如果是这样,内部选择是否需要是select for update

igetnqfo

igetnqfo1#

虽然欧文的建议可能是获得正确行为的“最简单”的方法(只要您在SQLSTATE为40001的情况下重试您的事务),排队应用程序本质上倾向于在请求阻塞以获得在队列中轮到它们的机会的情况下比在SERIALIZABLE事务的PostgreSQL实现中工作得更好,这允许更高的并发性并且对冲突的可能性稍微更“乐观”。
问题中的示例查询在默认的READ COMMITTED事务隔离级别下将允许两个(或更多)并发连接从队列中“认领”同一行。

  • T1启动,直到在UPDATE阶段锁定该行。
  • T2在执行时间上与T1重叠,并尝试更新该行。它阻塞T1的挂起的COMMITROLLBACK
  • T1提交,成功地“认领”了该行。
  • T2尝试更新该行,发现T1已经更新了该行,查找该行的新版本,发现它仍然满足选择标准(仅id匹配),并且还“声明”该行。

可以修改它以使其正常工作(如果您使用的PostgreSQL版本允许在子查询中使用FOR UPDATE子句)。只需将FOR UPDATE添加到选择id的子查询的末尾,就会发生以下情况:

  • T1启动,现在在 * 选择 * id之前锁定该行。
  • T2在执行时间上与T1重叠,并且在尝试选择id时阻塞,从而挂起T1的COMMITROLLBACK
  • T1提交,成功地“认领”了该行。
  • 当T2能够 * 读取 * 该行以查看id时,它发现该行已被声明,因此它找到下一个可用的id。

REPEATABLE READSERIALIZABLE事务隔离级别,写入冲突将引发错误,您可以捕获该错误并根据SQLSTATE确定序列化失败,然后重试。
如果您通常需要SERIALIZABLE事务,但又希望避免在队列区域中重试,则可以通过使用建议锁来实现这一点。

vwkv1x7d

vwkv1x7d2#

如果您是only用户,则查询应该没有问题。特别是,查询本身(外部查询和子查询之间)没有争用条件或死锁。手册:
但是,事务永远不会与自身冲突。
对于并发使用,事情可能会更复杂,安全起见,您可以使用SERIALIZABLE交易模式:

BEGIN ISOLATION LEVEL SERIALIZABLE;
UPDATE stuff
SET    computed = 'working'
WHERE  id = (SELECT id FROM stuff WHERE computed IS NULL LIMIT 1)
RETURNING * 
COMMIT;

您需要为序列化失败做好准备,并在这种情况下重试查询。
但我不完全确定这是否有点过头。我会让@kgrittn停下来..他是并发和可序列化事务的Maven..
And he did. :)

两全其美

在默认事务处理模式READ COMMITTED下运行查询。
对于Postgres 9.5或更高版本,请使用FOR UPDATE SKIP LOCKED。请参见:

对于旧版本,请在外部UPDATE中明确地重新检查条件computed IS NULL

UPDATE stuff
SET    computed = 'working'
WHERE  id = (SELECT id FROM stuff WHERE computed IS NULL LIMIT 1)
**AND   computed IS NULL**;

正如@kgrittn在他的回答的评论中所建议的那样,这个查询可能是空的,没有做任何事情,在(不太可能的)情况下,它与并发事务交织在一起。
因此,它的工作方式与事务模式SERIALIZABLE中的第一个变体非常相似,您必须重试-只是没有性能损失。
唯一的问题是:虽然冲突发生的可能性很小,因为机会窗口实在太小了,但在负载很重的情况下也可能发生,您无法确定是否最终没有剩余的行。
如果这并不重要(就像你的情况),你在这里完成。
如果是这样的话,为了 * 绝对肯定 *,在得到一个空结果后,再启动一个带有 * 显式锁定 * 的查询。如果这个结果是空的,那么你就完成了。如果不是,继续。
在plpgsql中,它可能如下所示:

LOOP
   UPDATE stuff
   SET    computed = 'working'
   WHERE  id = (SELECT id FROM stuff WHERE computed IS NULL
                LIMIT 1 **FOR UPDATE SKIP LOCKED**);  -- pg 9.5+
   -- WHERE  id = (SELECT id FROM stuff WHERE computed IS NULL LIMIT 1)
   -- **AND    computed IS NULL**; -- pg 9.4-

   **CONTINUE WHEN FOUND;**  -- continue outside loop, may be a nested loop

   UPDATE stuff
   SET    computed = 'working'
   WHERE  id = (SELECT id FROM stuff WHERE computed IS NULL
                LIMIT 1 **FOR UPDATE**);

   **EXIT WHEN NOT FOUND;**  -- exit function (end)
END LOOP;

这应该能让你两全其美:性能和可靠性。

相关问题