2

我有几个查询的交易。首先,选择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锁定几行然后等待条件中的其他行被解锁吗?

4

1 回答 1

3

SELECT FOR UPDATE不能防止死锁。它只是锁定行。沿途获取锁,按 指示的顺序ORDER BY,或在没有 的情况下按任意顺序ORDER BY。防止死锁的最佳方法是在整个事务中以一致的顺序锁定行 - 并在所有并发事务中这样做。或者,正如手册所说

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

否则,这可能会发生(row1row2,...是根据虚拟一致顺序编号的行):

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 NULLUPDATE队列中。(无论如何似乎都是个好主意?)

我假设它可能是由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;

手册:

使用NOWAIT时,如果无法立即锁定所选行,则该语句将报告错误,而不是等待。

如果您无论如何都无法与 UPSERT 建立一致的顺序,您甚至可以跳过该ORDER BY子句。NOWAIT

然后您必须捕获该错误并重试事务。类似于捕获序列化失败,但更便宜 - 并且不太可靠。例如,多个事务仍然可以单独与它们的 UPSERT 互锁。但这种可能性越来越小。

于 2020-08-04T22:27:10.153 回答