我正在构建各种排队机制。有需要处理的数据行和状态标志。我正在使用update .. returning
子句来管理它:
UPDATE stuff
SET computed = 'working'
WHERE id = (SELECT id from STUFF WHERE computed IS NULL LIMIT 1)
RETURNING *
嵌套选择部分与更新锁相同,或者我在这里有竞争条件吗?如果是这样,内部选择是否必须是select for update
?
答案 0 :(得分:32)
虽然Erwin的建议可能是最简单的方式来获得正确的行为(只要你在4000 SQLSTATE
获得异常时重试你的事务),按其性质对应用程序进行排队与阻止有机会轮流在队列中的请求相比,SERIALIZABLE
事务的PostgreSQL实现更容易工作,这允许更高的并发性,并且对碰撞的可能性更加“乐观”。
问题中的示例查询(在默认的READ COMMITTED
事务隔离级别中)将允许两个(或更多)并发连接“声明”队列中的同一行。会发生什么:
UPDATE
阶段中的行。COMMIT
或ROLLBACK
。id
匹配),还有“声明” “这一行。可以将其修改为正常工作(如果您使用的是允许子查询中的FOR UPDATE
子句的PostgreSQL版本)。只需将FOR UPDATE
添加到选择id的子查询的末尾,就会发生这种情况:
COMMIT
或ROLLBACK
。在REPEATABLE READ
或SERIALIZABLE
事务隔离级别,写入冲突会引发错误,您可以捕获并确定基于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停下来..他是 专家,有并发和可序列化的交易..
以默认交易模式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;
这应该会给你两全其美:性能和可靠性。