这个Postgres如何解决僵局?

时间:2018-03-05 23:22:26

标签: 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'与自身冲突并导致死锁。

以下是日志中的条目。似乎日志只显示死锁中涉及的1个进程的语句。由于此函数有两个查询,我不确定进程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)

1 个答案:

答案 0 :(得分:1)

看起来至少有一个竞争条件可能存在死锁(无论如何都是默认的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

通常,在WHERE查询和FOR UPDATE语句中重复相同的UPDATE条件会使您容易受到这些类型的死锁的影响。避免此问题的一般模式是让您的锁定查询返回一些明确的行标识符(如果您有一个生成的主键,或者失败,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值。它们不能被视为通用行标识符,因为它们可以通过各种内部操作进行更改,但只要您对行进行锁定它们就应该是稳定的。