涉及 SELECT FOR UPDATE 的死锁
Posted
技术标签:
【中文标题】涉及 SELECT FOR UPDATE 的死锁【英文标题】:Deadlock involving SELECT FOR UPDATE 【发布时间】:2020-11-24 22:25:22 【问题描述】:我有几个查询的交易。首先,选择带有FOR UPDATE
锁的行:
SELECT f.source_id FROM files AS f WHERE
f.component_id = $1 AND
f.archived_at IS NULL
FOR UPDATE
接下来是更新查询:
UPDATE files AS f SET archived_at = NOW()
WHERE
hw_component_id = $1 AND
f.source_id = ANY($2::text[])
然后是插入:
INSERT INTO files AS f (
source_id,
...
)
VALUES (..)
ON CONFLICT (component_id, source_id) DO UPDATE
SET archived_at = null,
is_valid = excluded.is_valid
我有两个应用程序实例,有时我会在 PostgreSQL 日志中看到死锁错误:
ERROR: deadlock detected
DETAIL: Process 3992939 waits for ShareLock on transaction 230221362; blocked by process 4108096.
Process 4108096 waits for ShareLock on transaction 230221365; blocked by process 3992939.
Process 3992939: SELECT f.source_id FROM files AS f WHERE f.component_id = $1 AND f.archived_at IS NULL FOR UPDATE
Process 4108096: INSERT INTO files AS f (source_id, ...) VALUES (..) ON CONFLICT (component_id, source_id) DO UPDATE SET archived_at = null, is_valid = excluded.is_valid
CONTEXT: while locking tuple (41116,185) in relation \"files\"
我认为这可能是由ON CONFLICT DO UPDATE
语句引起的,它可能会更新之前的SELECT FOR UPDATE
未锁定的行
但我不明白如果SELECT ... FOR UPDATE
查询是事务中的第一个查询,它怎么会导致死锁。在它之前没有查询。
SELECT ... FOR UPDATE
语句可以锁定几行,然后等待其他有条件的行被解锁吗?
【问题讨论】:
select...for update
正是这样做的。如果您不希望它等待锁定的行,请在末尾添加skip locked
。请参阅文档:postgresql.org/docs/current/…
@MikeOrganek: SKIP LOCKED
似乎是错误的工具。你可能在想NOWAIT
?
@ErwinBrandstetter 一个甚至一个超时都可能是合适的。很难判断 OP 试图用不连贯的 sn-ps 代码来完成什么。
SKIP LOCKED
仅适用于排队类型的查询,您继续使用实际锁定的行。似乎不适用。
@ErwinBrandstetter 在select. . . for update
运行之前,其他一些进程正在锁定行。在我看来,无论 OP 正在做什么,都应该重构为排队操作,但同样,我在这里没有看到足够的信息来说明情况。告诉我我的 cmets 错了,我会删除它们。
【参考方案1】:
SELECT FOR UPDATE
不能防止死锁。它只是锁定行。沿途获取锁,按照ORDER BY
指示的顺序,或者在没有ORDER BY
的情况下以任意顺序获取。防止死锁的最佳方法是在整个事务中以一致的顺序锁定行 - 并在所有并发事务中这样做。或者,作为the manual puts it:
防止死锁的最佳方法通常是通过以下方式避免死锁 确保所有使用数据库的应用程序都获得锁定 多个对象以一致的顺序。
否则,可能会发生这种情况(row1, row2, ... 是根据虚拟一致顺序编号的行):
T1: SELECT FOR UPDATE ... -- lock row2, row3
T2: SELECT FOR UPDATE ... -- lock row4, wait for T1 to release row2
T1: INSERT ... ON CONFLICT ... -- wait for T2 to release lock on row4
--> deadlock
将 ORDER BY
添加到您的 SELECT... FOR UPDATE
可能 已经避免了您的死锁。 (它会避免上面演示的那个。)或者发生这种情况,你必须做更多:
T1: SELECT FOR UPDATE ... -- lock row2, row3
T2: SELECT FOR UPDATE ... -- lock row1, wait for T1 to release row2
T1: INSERT ... ON CONFLICT ... -- wait for T2 to release lock on row1
--> deadlock
事务中的所有事情都必须以一致的顺序发生才能绝对确定。
另外,您的UPDATE
似乎与SELECT FOR UPDATE
不一致。 component_id
hw_component_id
。错别字?
此外,f.archived_at IS NULL
不保证后面的SET archived_at = NOW()
只影响这些行。您必须将WHERE f.archived_at IS NULL
添加到UPDATE
中。 (无论如何,这似乎是个好主意?)
我认为这可能是由
ON CONFLICT DO UPDATE
语句引起的, 这可能会更新未被先前SELECT FOR UPDATE
锁定的行。
只要 UPSERT (ON CONFLICT DO UPDATE
) 坚持一致的顺序,那将不是问题。但这可能很难或不可能执行。
SELECT ... FOR UPDATE
语句可以锁定几行,然后等待条件中的其他行解锁吗?
是的,如上所述,锁是一路获取的。它可能不得不停下来等待中途。
NOWAIT
如果仍然无法解决您的死锁,缓慢而可靠的方法是使用Serializable Isolation Level。然后你必须为序列化失败做好准备,并在这种情况下重试事务。总体来说要贵很多。
或者添加NOWAIT
就足够了:
SELECT FROM files
WHERE component_id = $1
AND archived_at IS NULL
ORDER BY id -- whatever you use for consistent, deterministic order
FOR UPDATE NOWAIT;
The manual:
使用
NOWAIT
,如果无法立即锁定选定的行,则语句将报告错误,而不是等待。
如果您无法与 UPSERT 建立一致的顺序,您甚至可以跳过带有 NOWAIT
的 ORDER BY
子句。
然后您必须捕获该错误并重试事务。类似于捕获序列化失败,但更便宜 - 并且不太可靠。例如,多个事务仍然可以单独与它们的 UPSERT 互锁。但这种可能性越来越小。
【讨论】:
感谢您的详细解释,它有很大帮助。但对我来说,仍然不清楚“一路上获得锁”的情况。我尝试在控制台中重现它,我开始了; SELECT FOR UPDATE where component_id=1 在一个控制台中,然后我在单独的事务中插入带有 component_id=1 的新记录。然后我开始新事务并执行BEGIN; SELECT FOR UPDATE 其中component_id=1。控制台预期冻结。然后我在我的第一个事务中运行更新查询,它成功执行,没有死锁错误。发生死锁的情况如何重现? 我忘记订购了。我将 ORDER BY 添加到 FOR UPDATE 语句,现在问题正在重现。谢谢!以上是关于涉及 SELECT FOR UPDATE 的死锁的主要内容,如果未能解决你的问题,请参考以下文章
select for update 并发insert死锁问题
同一张表上的两个“SELECT FOR UPDATE”语句会导致死锁吗?
在 SELECT ... INNER JOIN ... FOR UPDATE 的情况下,字符串的顺序是啥锁定以及如何避免死锁?