Postgres中的Atomic UPDATE .. SELECT

时间:2012-07-18 00:02:45

标签: multithreading postgresql concurrency race-condition transaction-isolation

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

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

嵌套选择部分与更新锁相同,或者我在这里有竞争条件吗?如果是这样,内部选择是否必须是select for update

2 个答案:

答案 0 :(得分:32)

虽然Erwin的建议可能是最简单的方式来获得正确的行为(只要你在4000 SQLSTATE获得异常时重试你的事务),按其性质对应用程序进行排队与阻止有机会轮流在队列中的请求相比,SERIALIZABLE事务的PostgreSQL实现更容易工作,这允许更高的并发性,并且对碰撞的可能性更加“乐观”。

问题中的示例查询(在默认的READ COMMITTED事务隔离级别中)将允许两个(或更多)并发连接“声明”队列中的同一行。会发生什么:

  • T1启动并获取锁定UPDATE阶段中的行。
  • T2在执行时间内重叠T1并尝试更新该行。它阻止等待T1的COMMITROLLBACK
  • T1承诺,成功“声称”了该行。
  • T2尝试更新行,发现T1已经拥有,查找该行的新版本,发现它仍然满足选择条件(只是id匹配),还有“声明” “这一行。

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

  • T1启动,现在在选择 id
  • 之前锁定行。
  • T2在执行时间内重叠T1并在尝试选择ID时阻止,等待T1的COMMITROLLBACK
  • T1承诺,成功“声称”了该行。
  • 当T2能够读取行以查看ID时,它会看到它已被声明,因此它会找到下一个可用的ID。

REPEATABLE READSERIALIZABLE事务隔离级别,写入冲突会引发错误,您可以捕获并确定基于SQLSTATE的序列化失败,然后重试。

如果您通常需要SERIALIZABLE交易,但又想避免在排队区域重试,则可以使用advisory lock来完成此操作。

答案 1 :(得分:19)

如果您是唯一用户,则查询应该没问题。特别是,查询本身(外部查询和子查询之间)没有竞争条件或死锁。我引用手册here

  

但是,交易永远不会与自身发生冲突。

对于并发使用,问题可能会更复杂。使用SERIALIZABLE transaction mode

,您将安全无虞
BEGIN ISOLATION LEVEL SERIALIZABLE;
UPDATE stuff
SET    computed = 'working'
WHERE  id = (SELECT id FROM stuff WHERE computed IS NULL LIMIT 1)
RETURNING * 
COMMIT;

您需要准备序列化失败并在这种情况下重试您的查询。

但我不完全确定这不是矫枉过正。我会请@kgrittn停下来..他是 专家,有并发和可序列化的交易..

And he did. :)


两全其美

以默认交易模式READ COMMITTED运行查询。

对于Postgres 9.5或更高版本,请使用FOR UPDATE SKIP LOCKED。参见:

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

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

正如@ kgrittn在对他的回答的评论中所建议的那样,这个查询可能在没有做任何事情的情况下变得空洞,在(不太可能的)情况下它与并发事务交织在一起。

因此,它与事务模式SERIALIZABLE中的第一个变体非常相似,您必须重试 - 只是没有性能损失。

唯一的问题:虽然冲突是不太可能的,因为机会之窗非常小,但它可能在重负荷下发生。你无法确定是否最后没有剩下的行。

如果这无关紧要(就像你的情况一样),你就完成了。
如果确实如此,那么绝对确定,在获得空结果后,再使用explicit locking再次开始查询。如果这个空了,你就完成了。如果没有,继续。
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;

这应该会给你两全其美:性能可靠性。