这个 Postgres 函数怎么会死锁?
How can this Postgres function deadlock?
我们的 postgres 数据库报告关系中的元组存在大量死锁。
只有两个函数使用该关系,通常只有一个函数涉及死锁。
最常导致死锁的函数有两个查询:
1. The first query
looks for ONE photo
and ROW LOCKS ALL the photo rows
for ALL albums the the photo is found in
For example given the below table of data:
if the query was looking for Photo 2
then it would LOCK ALL 6 rows of Album A and C.
album photo version
A 1 1.0 lock
A 2 1.0 lock update
A 3 1.0 lock
B 8 2.0
B 9 2.0
C 1 1.1 lock
C 2 1.1 lock update
C 5 1.1 lock
D 7 4.0
D 8 4.0
2. The second query then updates the 2 tuples for Photo 2.
FOR UPDATE 和 UPDATE 查询使用以下查询以相同顺序访问元组。
据我了解,如果始终按相册和照片顺序访问元组,则不会出现死锁。
该函数每秒被调用多次,我确实预料到会发生阻塞,但无法解释死锁。
感谢任何帮助。
函数中的查询 'album_version_set'
PERFORM 1
FROM work.album a
WHERE EXISTS (
SELECT
x.album
FROM work.album x
WHERE
x.photo = 2
AND x.album = a.album)
ORDER BY
a.album,
a.photo
FOR UPDATE;
WITH cte_update_version (album) AS (
UPDATE work.album a
SET
version = version + .1
FROM (
SELECT
x.album,
x.photo
FROM work.album x
WHERE
x.photo = 2
ORDER BY
x.album
x.photo
) ord
WHERE
a.album = ord.album
AND a.photo = ord.photo
RETURNING
a.album)
INSERT INTO tmp_album_keys(
album)
SELECT DISTINCT
us.album
FROM
cte_update_version;
为这个问题添加更多内容:
从错误日志中我可以看出函数 'album_version_set' 与自身冲突并导致死锁。
以下是日志中的条目。日志似乎只显示了死锁中涉及的进程之一的语句。由于此函数有两个查询,我不确定进程 31019 中的哪个查询是死锁的一部分。
这是日志中的一个条目:
2018-03-06 15:35:20 UTC:10.1.2.1(43636):z1login@atier:[31024]:ERROR: deadlock detected
2018-03-06 15:35:20 UTC:10.1.2.1(43636):z1login@atier:[31024]:DETAIL: Process 31024 waits for ShareLock on transaction 8334317; blocked by process 31019.
Process 31019 waits for ShareLock on transaction 8334322; blocked by process 31024.
Process 31024: SELECT * FROM album_version_set(, )
Process 31019: SELECT * FROM album_version_set(, )
2018-03-06 15:35:20 UTC:10.1.2.1(43636):z1login@atier:[31024]:HINT: See server log for query details.
2018-03-06 15:35:20 UTC:10.1.2.1(43636):z1login@atier:[31024]:CONTEXT: while locking tuple (11,83) in relation "album"
SQL statement "SELECT 1
FROM work.album a
WHERE EXISTS (
SELECT
x.album
FROM work.album x
WHERE
x.photo = 2
AND x.album = a.album)
ORDER BY
a.album,
a.photo
FOR UPDATE;"
PL/pgSQL function album_version_set(character varying,smallint) line 69 at PERFORM
2018-03-06 15:35:20 UTC:10.1.2.1(43636):z1login@atier:[31024]:STATEMENT: SELECT * FROM album_version_set(, )
看起来至少有一个竞争条件可能会导致死锁(默认情况下 transaction isolation level,无论如何),但我不能确定它是你的原因。
假设您的 table 最初看起来像这样:
album photo version
B 2 1.0
C 2 1.0
您的第一个查询运行,并开始锁定行。
与此同时,其他人运行 INSERT INTO work.album VALUES ('A', 2, 1.0)
。
这个新行被 FOR UPDATE
查询忽略(因为它的数据库快照固定在语句的开头),但它仍然被后续的 UPDATE
拾取,并且在进程中被锁定。
总体而言,您交易中的锁定顺序(根据 album
值)为 'B'
、'C'
、'A'
;您现在面临死锁的风险。
更糟糕的是,如果并发插入包含多行,那么您已经用 photo = 2
更新了记录而没有锁定相册的其余部分。例如,如果并发语句是 INSERT INTO work.album VALUES ('A', 2, 1.0), ('A', 3, 1.0)
,那么您将处于以下状态:
album photo version
A 2 1.0 update
A 3 1.0
B 2 1.0 lock update
C 2 1.0 lock update
一般来说,在您的 FOR UPDATE
查询和 UPDATE
语句中重复相同的 WHERE
条件会使您容易受到此类死锁的影响。避免这个问题的一般模式是让你的锁定查询 return 一些明确的行标识符(如果你有一个生成的主键,或者没有,ctid
*)来明确到底是什么已被锁定,然后将这些标识符传递给 UPDATE
语句以确保它仅针对锁定的元组,例如:
DECLARE
locked_tuples tid[];
BEGIN
locked_tuples := ARRAY(
SELECT ctid
FROM work.album
WHERE album IN (
SELECT x.album
FROM work.album x
WHERE x.photo = 2
)
ORDER BY album, photo
FOR UPDATE
);
WITH cte_update_version (album) AS (
UPDATE work.album
SET version = version + .1
WHERE
ctid = ANY(locked_tuples) AND
photo = 2
RETURNING album
)
INSERT INTO tmp_album_keys(album)
SELECT DISTINCT album
FROM cte_update_status;
END
这应该可以消除死锁的可能性,尽管这也意味着同时插入的行将不再更新(这可能是也可能不是您所希望的)。
* 请注意 ctid
值。它们不能被视为 general-purpose 行标识符,因为它们可以通过各种内部操作更改,但只要您持有该行的锁,它们就应该是 stable。
我们的 postgres 数据库报告关系中的元组存在大量死锁。 只有两个函数使用该关系,通常只有一个函数涉及死锁。
最常导致死锁的函数有两个查询:
1. The first query
looks for ONE photo
and ROW LOCKS ALL the photo rows
for ALL albums the the photo is found in
For example given the below table of data:
if the query was looking for Photo 2
then it would LOCK ALL 6 rows of Album A and C.
album photo version
A 1 1.0 lock
A 2 1.0 lock update
A 3 1.0 lock
B 8 2.0
B 9 2.0
C 1 1.1 lock
C 2 1.1 lock update
C 5 1.1 lock
D 7 4.0
D 8 4.0
2. The second query then updates the 2 tuples for Photo 2.
FOR UPDATE 和 UPDATE 查询使用以下查询以相同顺序访问元组。
据我了解,如果始终按相册和照片顺序访问元组,则不会出现死锁。
该函数每秒被调用多次,我确实预料到会发生阻塞,但无法解释死锁。
感谢任何帮助。
函数中的查询 'album_version_set'
PERFORM 1
FROM work.album a
WHERE EXISTS (
SELECT
x.album
FROM work.album x
WHERE
x.photo = 2
AND x.album = a.album)
ORDER BY
a.album,
a.photo
FOR UPDATE;
WITH cte_update_version (album) AS (
UPDATE work.album a
SET
version = version + .1
FROM (
SELECT
x.album,
x.photo
FROM work.album x
WHERE
x.photo = 2
ORDER BY
x.album
x.photo
) ord
WHERE
a.album = ord.album
AND a.photo = ord.photo
RETURNING
a.album)
INSERT INTO tmp_album_keys(
album)
SELECT DISTINCT
us.album
FROM
cte_update_version;
为这个问题添加更多内容:
从错误日志中我可以看出函数 'album_version_set' 与自身冲突并导致死锁。
以下是日志中的条目。日志似乎只显示了死锁中涉及的进程之一的语句。由于此函数有两个查询,我不确定进程 31019 中的哪个查询是死锁的一部分。
这是日志中的一个条目:
2018-03-06 15:35:20 UTC:10.1.2.1(43636):z1login@atier:[31024]:ERROR: deadlock detected
2018-03-06 15:35:20 UTC:10.1.2.1(43636):z1login@atier:[31024]:DETAIL: Process 31024 waits for ShareLock on transaction 8334317; blocked by process 31019.
Process 31019 waits for ShareLock on transaction 8334322; blocked by process 31024.
Process 31024: SELECT * FROM album_version_set(, )
Process 31019: SELECT * FROM album_version_set(, )
2018-03-06 15:35:20 UTC:10.1.2.1(43636):z1login@atier:[31024]:HINT: See server log for query details.
2018-03-06 15:35:20 UTC:10.1.2.1(43636):z1login@atier:[31024]:CONTEXT: while locking tuple (11,83) in relation "album"
SQL statement "SELECT 1
FROM work.album a
WHERE EXISTS (
SELECT
x.album
FROM work.album x
WHERE
x.photo = 2
AND x.album = a.album)
ORDER BY
a.album,
a.photo
FOR UPDATE;"
PL/pgSQL function album_version_set(character varying,smallint) line 69 at PERFORM
2018-03-06 15:35:20 UTC:10.1.2.1(43636):z1login@atier:[31024]:STATEMENT: SELECT * FROM album_version_set(, )
看起来至少有一个竞争条件可能会导致死锁(默认情况下 transaction isolation level,无论如何),但我不能确定它是你的原因。
假设您的 table 最初看起来像这样:
album photo version
B 2 1.0
C 2 1.0
您的第一个查询运行,并开始锁定行。
与此同时,其他人运行 INSERT INTO work.album VALUES ('A', 2, 1.0)
。
这个新行被 FOR UPDATE
查询忽略(因为它的数据库快照固定在语句的开头),但它仍然被后续的 UPDATE
拾取,并且在进程中被锁定。
总体而言,您交易中的锁定顺序(根据 album
值)为 'B'
、'C'
、'A'
;您现在面临死锁的风险。
更糟糕的是,如果并发插入包含多行,那么您已经用 photo = 2
更新了记录而没有锁定相册的其余部分。例如,如果并发语句是 INSERT INTO work.album VALUES ('A', 2, 1.0), ('A', 3, 1.0)
,那么您将处于以下状态:
album photo version
A 2 1.0 update
A 3 1.0
B 2 1.0 lock update
C 2 1.0 lock update
一般来说,在您的 FOR UPDATE
查询和 UPDATE
语句中重复相同的 WHERE
条件会使您容易受到此类死锁的影响。避免这个问题的一般模式是让你的锁定查询 return 一些明确的行标识符(如果你有一个生成的主键,或者没有,ctid
*)来明确到底是什么已被锁定,然后将这些标识符传递给 UPDATE
语句以确保它仅针对锁定的元组,例如:
DECLARE
locked_tuples tid[];
BEGIN
locked_tuples := ARRAY(
SELECT ctid
FROM work.album
WHERE album IN (
SELECT x.album
FROM work.album x
WHERE x.photo = 2
)
ORDER BY album, photo
FOR UPDATE
);
WITH cte_update_version (album) AS (
UPDATE work.album
SET version = version + .1
WHERE
ctid = ANY(locked_tuples) AND
photo = 2
RETURNING album
)
INSERT INTO tmp_album_keys(album)
SELECT DISTINCT album
FROM cte_update_status;
END
这应该可以消除死锁的可能性,尽管这也意味着同时插入的行将不再更新(这可能是也可能不是您所希望的)。
* 请注意 ctid
值。它们不能被视为 general-purpose 行标识符,因为它们可以通过各种内部操作更改,但只要您持有该行的锁,它们就应该是 stable。