2

在一个会话中,我尝试锁定“users”表中的多行,并获取 user 的“status”列WHERE id = 2

do $$
declare user_status int;
begin
    WITH t(id, status) AS(
        SELECT id, status FROM users WHERE id in( 2,4,7,6) order by id FOR UPDATE
    )
    SELECT status FROM t WHERE id = 2 INTO user_status;

    -- just run transaction for a while
    FOR i in 1..2000000000 loop
    end loop;

end;
$$ language plpgsql

在此事务运行时,我在另一个会话中运行此查询:

UPDATE users SET some_col = some_col WHERE id = 6;

我希望该行WHERE id = 6应该被第一个事务锁定,但这不是真的,因为UPDATE在第二个会话中立即运行(不等待在第一个会话中结束事务)。

我有什么误解?

附言

如果在第一次交易中,而不是 CTE 使用:

perform id, status FROM users WHERE id in( 2,4,7,6) order by id FOR UPDATE;

然后锁按预期工作。

4

1 回答 1

1

我认为这是由于 PL/pgSQLSELECT INTO在后台的工作方式。

文档中提到了明显的根本原因:

如果在游标的查询中使用了锁定子句,则只有游标实际获取或跳过的行才会被锁定。

...并且这种效果会流向查询中的任何 CTE,其中只有在外部查询需要行时才会获取(并因此锁定)行。

这很容易证明。首先,一些设置:

CREATE TABLE t (x INT);
INSERT INTO t VALUES (1),(2),(3);

此查询将按预期锁定所有行:

WITH cte AS (SELECT x FROM t WHERE x IN (1,2,3) FOR UPDATE)
SELECT x FROM cte WHERE x = 1

但是,这将产生您观察到的行为,仅锁定第一条记录:

DECLARE c CURSOR FOR
  WITH cte AS (SELECT x FROM t WHERE x IN (1,2,3) FOR UPDATE)
  SELECT x FROM cte WHERE x = 1;
FETCH NEXT FROM c;

我的猜测是SELECT INTOPL/pgSQL 中的 a 以几乎相同的方式运行。如文档中所述,如果查询返回多行,则将第一行分配给目标变量,其余的将被忽略(因此它永远不需要获取多个记录)。

但是,此语句锁定所有行t

DO $$
DECLARE i INT;
BEGIN
  WITH cte AS (SELECT x FROM t WHERE x IN (1,2,3) FOR UPDATE)
  SELECT x FROM cte WHERE x = 1 INTO STRICT i;
END
$$ LANGUAGE plpgsql;

注意使用SELECT INTO STRICT. 如果查询返回多行,STRICT则关键字会引发错误。但是为了发现返回了第二行,PL/pgSQL 必须尝试获取它,这会导致 CTE 查询获取剩余的记录(将它们锁定在进程中)。

在 PL/pgSQL 函数中,所有这些大部分时间都不是问题,因为您通常会从查询中获取所有FOR UPDATE记录,并将它们传递给实际更新它们的人。另一方面,如果您想要查询的副作用但对其输出不感兴趣,那么(如您所见)您可以使用PERFORM,它将运行查询以完成,但丢弃结果。

在极少数情况下,您需要(即锁定)所有行,同时只返回其中的一部分,您可能需要编写自己的循环

于 2018-01-30T13:57:03.917 回答