我有一个简单的表格,如下所示:
Table "public.test"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
id | integer | | not null |
name | text | | |
Indexes:
"ss_pkey" PRIMARY KEY, btree (id)
该表当前有两行:
id | name
----+---------
0 | Matthew
1 | Thomas
(2 rows)
现在我有两个事务并发运行:
Transaction 1
BEGIN;
WITH locked_rows AS(
SELECT *
FROM TEST
FOR UPDATE
)
INSERT INTO TEST
VALUES
(1, 'Chris'),
(0, 'John')
ON CONFLICT(id) DO
UPDATE
SET name = excluded.name
WHERE test.name <> excluded.name
COMMIT;
Transaction 2
BEGIN;
WITH locked_rows AS(
SELECT *
FROM TEST
FOR UPDATE
)
INSERT INTO TEST
VALUES
(0, 'Bryan'),
(1, 'Steven')
ON CONFLICT(id) DO
UPDATE
SET name = excluded.name
WHERE test.name <> excluded.name
COMMIT;
要插入的值有意地以相反的顺序插入事务之间,因为我想触发死锁,看看是否可以通过使用SELECT...FOR UPDATE;
来避免。现在我明白了,改变顺序可以避免僵局,但我还是想知道这里发生了什么。
当这两个事务并发执行时,它们总是死锁,并出现错误:
ERROR: deadlock detected
DETAIL: Process 29588 waits for ShareLock on transaction 255002; blocked by process 29010.
Process 29010 waits for ShareLock on transaction 255003; blocked by process 29588.
HINT: See server log for query details.
CONTEXT: while inserting index tuple (0,27) in relation "test"
SQL state: 40P01
但是如果我移除CTE并简单地将每个事务分解成两个单独的命令,例如:
BEGIN;
SELECT * FROM TEST FOR UPDATE;
INSERT INTO TEST
VALUES
(1, 'Chris'),
(0, 'John')
ON CONFLICT(id) DO
UPDATE
SET name = excluded.name
WHERE test.name <> excluded.name
COMMIT;
另一个事务也是如此,那么死锁消失,第二个事务只需等待第一个事务释放行上的锁,这也是我所期望的。
我希望在使用CTE时也会发生同样的情况,因为SELECT...FOR UPDATE
仍然在事务中执行,但事实并非如此。我还尝试声明CTE AS MATERIALIZED
,看看这是否会有所不同,但没有任何变化。从Postgres的文档中,我看到了这样的内容:
如果在锁定子句中指定了特定的表,则只锁定来自这些表的行; SELECT中使用的任何其他表都可以像往常一样简单地读取。没有表列表的锁定子句会影响语句中使用的所有表。如果将锁定子句应用于视图或子查询,则它会影响视图或子查询中使用的所有表。但是,这些子句不适用于主查询引用的WITH查询。如果希望在WITH查询中发生行锁定,请在WITH查询中指定锁定子句。
但我不太明白它的意思,我想这是指FOR UPDATE
中的[ OF table_name [, ...] ]
可选参数。
postgres是否完全忽略CTE,因为它不会在后续查询中使用?如果是这样的话,那么为什么其他命令(如DELETE
或UPDATE
)在未使用的CTE中正常工作?还是在幕后发生了什么?
注意:我使用的是PostgreSQL 15.2
2条答案
按热度按时间hs1ihplo1#
由于CTE永远不会执行,因此您会出现死锁。
CTE仅在包含DML语句或在main语句中使用时才会执行。参见the documentation:
WITH
中的数据修改语句只执行一次,并且始终执行到完成,与主查询是否读取所有(或实际上任何)它们的输出无关。请注意,这与WITH
中SELECT
的规则不同:如前一节所述,SELECT
的执行仅在主查询要求其输出的情况下进行。使用
SELECT ... FOR UPDATE
避免死锁的两个注意事项:1.除非计划删除该行或修改键,否则应使用
FOR NO KEY UPDATE
以避免过度锁定SELECT ... FROM tab FOR NO KEY UPDATE
的两个会话仍可能彼此死锁synchronize_seqscans
参数)SELECT
的快照以不同的顺序查看行ubby3x7f2#
两个事务都试图同时获取同一行上的
exclusive
锁。在第一个事务中,您使用
CTE
从测试表中选择所有行,并使用FOR UPDATE.
锁定它们。这意味着第一个事务将获取对测试表中所有行的exclusive
锁。在第二个事务中,您还使用CTE从测试表中选择所有行。但是,此CTE不使用
FOR UPDATE
子句。这意味着第二个事务将只获取测试表中的shared lock on the rows
。当第二个事务尝试更新已被第一个事务锁定的行时,将发生
deadlock
。要避免此死锁,您可以:
FOR UPDATE
子句在
CTEs
中使用FOR UPDATE
子句执行选择和更新的单个事务:
第二个
CTE
也使用FOR UPDATE
子句。但是,deadlock
仍然发生,因为第二个事务正在尝试获取第一个事务已经锁定的行上的exclusive
锁。第二个事务能够在测试表中的行上获取
shared loc
k的原因是因为第一个事务已经在这些行上获取了独占锁。这是因为FOR UPDATE
子句是blocking lock.
这意味着第二个事务将等待直到第一个事务释放行上的排他锁,然后才能获取这些行上的共享锁。但是,当第二个事务尝试更新已经被第一个事务锁定的行时,会发生死锁,因为第二个事务现在正试图获取该行的
exclusive
锁。这将导致第一个事务阻塞,第二个事务阻塞。这将产生无法解决的deadlock
情况。为了避免这种死锁,可以在两个CTE中使用
FOR UPDATE
子句,也可以使用单个事务来执行选择和更新。