涉及 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 建立一致的顺序,您甚至可以跳过带有 NOWAITORDER 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引发死锁分析

select for update 并发insert死锁问题

同一张表上的两个“SELECT FOR UPDATE”语句会导致死锁吗?

在 SELECT ... INNER JOIN ... FOR UPDATE 的情况下,字符串的顺序是啥锁定以及如何避免死锁?

Mysql查询语句使用select.. for update导致的数据库死锁分析

postgresql是多行原子的SELECT FOR UPDATE吗?