postgresql - 这个 Postgres 函数怎么会死锁?

标签 postgresql concurrency locking deadlock blocking

我们的 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($1, $2)
    Process 31019: SELECT * FROM album_version_set($1, $2)
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($1, $2)

最佳答案

看起来至少有一个竞争条件可能会导致死锁(默认情况下是 transaction isolation level ,无论如何),但我不能确定它会导致你的问题。

假设您的表格最初看起来像这样:

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 条件会使您容易受到这些类型的死锁的影响.避免此问题的一般模式是让您的锁定查询返回一些明确的行标识符(如果有生成的主键,如果没有,则为 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 值。它们不能被视为通用行标识符,因为它们可以通过各种内部操作进行更改,但只要您持有该行的锁,它们就应该是稳定的。

关于postgresql - 这个 Postgres 函数怎么会死锁?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49121102/

相关文章:

java - LinkedList并发修改异常

sql - 如何停止 Sql Server 的用户实例? (Sql Express 用户实例数据库文件被锁定,即使在停止 Sql Express 服务后)

sqlite - .backup 期间是否锁定了 sqlite

sql - 如何获取 'DD-Mon-YYYY' 格式的 postgres sql 时间戳?

json - 从 postgres 中的复杂嵌套结构中检索具有特定键名的 json 元素

php - Laravel 在查询 JSONB 列(转换为数组)时是否调用 json_decode 一次或多次?

sql - 为什么左连接会导致优化器忽略索引?

java - 如果一个方法是同步的,那么被调用的方法是否也必须同步?

c++ - std::shared_ptr 和 std::experimental::atomic_shared_ptr 有什么区别?

java - 如果使用不同的对象进行同步,更改对不同的线程是否可见?