带有 ORDER BY 的 Postgres UPDATE,怎么做?

Postgres UPDATE with ORDER BY, how to do it?

我需要对一组记录进行 Postgres 更新,并且我正在努力防止出现在压力测试中的死锁。

对此的典型解决方案是按特定顺序更新记录,例如按 ID - 但似乎 Postgres 不允许 ORDER BY 进行更新。

假设我需要进行更新,例如:

UPDATE BALANCES WHERE ID IN (SELECT ID FROM some_function() ORDER BY ID);

当您同时 运行 200 个查询时会导致死锁。怎么办?

我正在寻找一个通用的解决方案,而不是像 UPDATE with ORDER BY

中那样的针对特定案例的解决方法

感觉肯定有比编写游标函数更好的解决方案。另外,如果没有更好的方法,该游标功能的最佳外观如何?逐条更新

总的来说,并发比较难。特别是对于 200 条语句(我假设您不仅查询 = SELECT)甚至交易(实际上,如果发出的每条语句不在交易中,它都被包装到交易中)。

一般的解决方案概念是(组合)这些:

  1. 要注意可能会发生死锁,请在应用程序中捕获死锁,检查 Error Codes 是否有 class 4040P01 并重试事务。

  2. 保留锁。使用 SELECT ... FOR UPDATE。尽可能长时间地避免显式锁定。锁会强制其他事务等待锁释放,这会损害并发性,但可以防止事务 运行 陷入死锁。检查第 13 章中的死锁示例。尤其是事务 A 等待 B 和 B 等待 A(银行帐户)的那个。

  3. 如果可能,请选择不同的 Isolation Level,例如较弱的 ​​READ COMMITED。请注意 READ COMMITED 模式下的 LOST UPDATEs。用 REPEATABLE READ.

  4. 来阻止它们

在每个事务中以相同的顺序编写带有锁的语句,例如按 table 名称字母顺序。

LOCK / USE A  -- Transaction 1 
LOCK / USE B  -- Transaction 1
LOCK / USE C  -- Transaction 1
-- D not used -- Transaction 1

-- A not used -- Transaction 2
LOCK / USE B  -- Transaction 2
-- C not used -- Transaction 2
LOCK / USE D  -- Transaction 2

与一般锁定顺序 A B C D。这样,事务可以以任何相对顺序交错,并且仍然有很好的机会不死锁(取决于您的语句,您可能还有其他序列化问题)。交易的语句将运行按照他们指定的顺序,但也可以是交易1运行是他们的第一个2,然后是xact 2运行是第一个,然后是1完成,最后 xact 2 完成。

此外,您应该意识到涉及多行的语句在并发情况下不会自动执行。换句话说,如果你有两条语句 A 和 B 涉及多行,那么它们可以按照这样的顺序执行:

a1 b1 a2 a3 a4 b2 b3     

但不是作为 a 后跟 b 的块。 这同样适用于带有子查询的语句。 您是否使用 EXPLAIN 查看了查询计划?

你的情况,你可以试试

UPDATE BALANCES WHERE ID IN (
 SELECT ID FROM some_function() FOR UPDATE  -- LOCK using FOR UPDATE 
 -- other transactions will WAIT / BLOCK temporarily on conc. write access
);

如果可能的话,您还可以使用 SELECT ... FOR UPDATE SKIP LOCK,这将跳过已经锁定的数据以取回并发性,并发性因等待另一个事务释放锁而丢失(FOR UPDATE ).但这不会将 UPDATE 应用于锁定的行,而您的应用程序逻辑可能需要这样做。所以 运行 稍后(见第 1 点)。

另请阅读 LOST UPDATE 关于 LOST UPDATESKIP LOCKED 大约 SKIP LOCKED。在您的情况下,队列可能是一个想法,SKIP LOCKED 参考中对此进行了完美解释,尽管关系 DBMS 并不意味着是队列。

HTH

据我所知,没有办法直接通过UPDATE语句来实现;保证锁顺序的唯一方法是使用 SELECT ... ORDER BY ID FOR UPDATE 显式获取锁,例如:

UPDATE Balances
SET Balance = 0
WHERE ID IN (
  SELECT ID FROM Balances
  WHERE ID IN (SELECT ID FROM some_function())
  ORDER BY ID
  FOR UPDATE
)

这具有在 Balances table 上重复 ID 索引查找的缺点。在您的简单示例中,您可以通过在锁定查询期间获取物理行地址(由 ctid system column 表示)并使用它来驱动 UPDATE:

来避免这种开销
UPDATE Balances
SET Balance = 0
WHERE ctid = ANY(ARRAY(
  SELECT ctid FROM Balances
  WHERE ID IN (SELECT ID FROM some_function())
  ORDER BY ID
  FOR UPDATE
))

(使用 ctids 时要小心,因为值是瞬态的。我们在这里很安全,因为锁会阻止任何更改。)

不幸的是,规划器只会在少数情况下使用 ctid(您可以通过在 EXPLAIN 输出中查找 "Tid Scan" 节点来判断它是否有效) .在单个 UPDATE 语句中处理更复杂的查询,例如如果 some_function() 将您的新余额与 ID 一起返回,您需要退回到基于 ID 的查找:

UPDATE Balances
SET Balance = Locks.NewBalance
FROM (
  SELECT Balances.ID, some_function.NewBalance
  FROM Balances
  JOIN some_function() ON some_function.ID = Balances.ID
  ORDER BY Balances.ID
  FOR UPDATE
) Locks
WHERE Balances.ID = Locks.ID

如果性能开销是一个问题,您需要求助于使用游标,它看起来像这样:

DO $$
DECLARE
  c CURSOR FOR
    SELECT Balances.ID, some_function.NewBalance
    FROM Balances
    JOIN some_function() ON some_function.ID = Balances.ID
    ORDER BY Balances.ID
    FOR UPDATE;
BEGIN
  FOR row IN c LOOP
    UPDATE Balances
    SET Balance = row.NewBalance
    WHERE CURRENT OF c;
  END LOOP;
END
$$