为什么单个 UPDATE 查询会出现死锁?

Joh*_*hir 3 postgresql deadlock concurrency transaction update

我有两个进程并行执行这样的代码:

begin;
update foos set unread=false where owner_id=123 and unread=true;
commit;
Run Code Online (Sandbox Code Playgroud)

这会导致死锁。

我对导致死锁的原因的理解就像这个问题中描述的场景,“交织” UPDATE 语句以不同的顺序更新两个不同的行。我不明白单个 UPDATE 语句如何导致死锁。我无法在我的开发环境中使用两个并行 psql 会话来复制死锁场景。我为什么不能复制它的猜测:

  1. 我误解了导致死锁错误的代码,并且每个事务中实际上有多个 UPDATE 语句
  2. “交织”方面正在发生,但“在”涵盖多行的 UPDATE 语句中,因此很难复制。

这个单一的 UPDATE 是否有可能造成死锁?

Lau*_*lbe 8

您的语句修改了几行。这些行中的每一行在更新时都会被锁定。

并发事务中的语句很可能已经锁定了这些行之一,从而阻塞了您的UPDATE. 如果并发事务随后尝试锁定您UPDATE已经锁定的行之一,则会出现死锁。


Erw*_*ter 6

Laurenz 解释了可能导致死锁的机制,您已经包含了 Kevin 更详细解释的链接:

以下是如何复制死锁的分步说明 - 与普通UPDATE的工作方式相同SELECT .. FOR UPDATE

现在,如何避免这个问题
如果您要更新大量共享表或所有表 - 并且您负担得起 - 只需对表进行写锁定。通常,这不是要走的路。否则,三种不同的方法:

1. 一致的顺序

该手册在关于死锁的章节中有这样的建议:

防止死锁的最佳方法通常是通过确定所有使用数据库的应用程序以一致的顺序获取多个对象的锁来避免它们。

不知道为什么仍然没有ORDER BYfor UPDATE。但这就是我们必须解决的问题。改为SELECT ... FOR UPDATE在同一事务中锁定行- 就像您已经尝试过的那样,正如您之前的问题所示。你只是忘记了基本的确定性ORDER BY

BEGIN;
SELECT FROM foos WHERE owner_id = 123 AND unread
ORDER  BY ??? -- any deterministic order, PK would be an obvious candidate
FOR    UPDATE;

UPDATE foos SET unread = false WHERE owner_id = 123 AND unread;
END;
Run Code Online (Sandbox Code Playgroud)

显然,所有潜在的竞争事务都必须以相同的顺序获取锁。

2. 跳过锁定的行

只处理未锁定的行:

BEGIN;
SELECT FROM foos WHERE owner_id = 123 AND unread
-- ORDER BY ???  -- optional in this case
FOR    UPDATE SKIP LOCKED;

UPDATE foos SET unread = false WHERE owner_id = 123 AND unread;
END;
Run Code Online (Sandbox Code Playgroud)

如果您确定跳过的行已由执行相同操作的竞争事务处理,则您在此处完成。(你确定吗?)
否则,为了确保,跟进检查:

SELECT EXISTS (SELECT FROM foos WHERE owner_id = 123 AND unread);
Run Code Online (Sandbox Code Playgroud)

作家不会阻止读者,读者不会阻止作家,所以这将返回,TRUE直到每一行都被成功更新。循环上面的UPDATE块,然后是这个(有适当的延迟),直到你得到FALSE. 然后你就完成了。

对于ORDER BY会增加大量成本的大型集,可能会更便宜。OTOH,ORDER BY如果有匹配的索引,添加仍然有意义......

3.一次一个

与上面类似,除了一次只更新一行。通常更昂贵,但任何潜在的死锁都会被消除 - 如果做得好。在处理单行已经需要很长时间时考虑这一点。

详细解释(大部分也适用于上述)和说明: