56

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

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

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

4

2 回答 2

43

虽然 Erwin 的建议可能是获得正确行为的最简单方法(只要您在遇到SQLSTATE40001 异常时重试事务),排队应用程序本质上往往更适合请求阻塞以有机会轮到他们与事务的 PostgreSQL 实现相比,队列SERIALIZABLE允许更高的并发性,并且对冲突的可能性更加“乐观”。

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

  • T1 开始并一直锁定UPDATE阶段中的行。
  • T2 在执行时间上与 T1 重叠,并尝试更新该行。它阻塞待处理的T1COMMITROLLBACK
  • 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 事务,但又想避免在排队区域重试,则可以使用咨询锁来实现。

于 2012-07-19T20:35:34.573 回答
28

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

但是,事务永远不会与自身发生冲突。

对于并发使用,事情可能会更复杂。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 停下来......他是并发和可序列化事务的专家......

他做到了。:)


两全其美

在默认事务模式下运行查询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,您必须重试 - 只是没有性能损失。

唯一的问题:虽然冲突不太可能发生,因为机会之窗是如此之小,但它可能在重负载下发生。您无法确定是否最终没有更多行了。

如果这无关紧要(就像您的情况一样),那么您就完成了。
如果确实如此,绝对可以肯定的是,在得到一个空结果后,再用显式锁定启动一个查询。如果这是空的,你就完成了。如果没有,请继续。
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;

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

于 2012-07-18T02:49:47.310 回答