sql - 我怎样才能避免 Postgres 中的这种三向死锁?

标签 sql postgresql deadlock

我在 Postgres 中遇到了三向死锁,我真的不明白是什么导致了它。日志消息是,

    Process 5671 waits for ExclusiveLock on tuple (33,12) of relation 31709 of database 16393; blocked by process 5652.
    Process 5652 waits for ShareLock on transaction 3382643; blocked by process 5670.
    Process 5670 waits for ExclusiveLock on tuple (33,12) of relation 31709 of database 16393; blocked by process 5671.
    Process 5671: UPDATE "user" SET
                    seqno = seqno + 1,
                    push_pending = true
                  WHERE user_id = $1
                  RETURNING seqno
    Process 5652: UPDATE "user" SET
                    seqno = seqno + 1,
                    push_pending = true
                  WHERE user_id = $1
                  RETURNING seqno
    Process 5670: UPDATE "user" SET
                    seqno = seqno + 1,
                    push_pending = true
                  WHERE user_id = $1
                  RETURNING seqno

(关系 31709 是 “user” 表。user_id 在所有三个事务中都相同。)

这看起来不像您看到的普通死锁 demonstrated in the documentation .我不会乱序更新此表的多行。我怀疑 RETURNING 子句与它有关,但我不明白为什么。关于如何解决这个问题或进一步调试它的任何想法?

更新 Erwin 在评论中的问题:这是 Postgres 9.3。此事务中还有其他命令,但我不认为它们会触及“用户”表。表上有一个触发器,用于使用 current_timestamp() 更新 updated_at 列:

CREATE OR REPLACE FUNCTION update_timestamp() RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = now();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

当我可以重现时,我会考虑获取交易的详细信息;我正在事后分析日志。

更新 #2:我用 LOCK_DEBUG 重建了 Postgres 并用 trace_locks=on 运行,试图在我的头脑中围绕顺序采取了哪些锁。

更新后的死锁消息是:

Process 54131 waits for ShareLock on transaction 4774; blocked by process 54157.
Process 54157 waits for ExclusiveLock on tuple (1,16) of relation 18150 of database 18136; blocked by process 54131.

我可以很清楚地看到 ExclusiveLock 上的阻塞:

2014-08-05 10:35:15 EDT apiary [54131] 2/316 4773 LOG:  00000: LockAcquire: new: lock(0x10f039f88) id(18136,18150,1,16,3,1) grantMask(0) req(0,0,0,0,0,0,0)=0 grant(0,0,0,0,0,0,0)=0 wait(0) type(ExclusiveLock)
2014-08-05 10:35:15 EDT apiary [54131] 2/316 4773 LOG:  00000: GrantLock: lock(0x10f039f88) id(18136,18150,1,16,3,1) grantMask(80) req(0,0,0,0,0,0,1)=1 grant(0,0,0,0,0,0,1)=1 wait(0) type(ExclusiveLock)
2014-08-05 10:35:15 EDT apiary [54157] 3/188 4774 LOG:  00000: LockAcquire: found: lock(0x10f039f88) id(18136,18150,1,16,3,1) grantMask(80) req(0,0,0,0,0,0,1)=1 grant(0,0,0,0,0,0,1)=1 wait(0) type(ExclusiveLock)
2014-08-05 10:35:15 EDT apiary [54157] 3/188 4774 LOG:  00000: WaitOnLock: sleeping on lock: lock(0x10f039f88) id(18136,18150,1,16,3,1) grantMask(80) req(0,0,0,0,0,0,2)=2 grant(0,0,0,0,0,0,1)=1 wait(0) type(ExclusiveLock)

(格式为date program [pid] virtualtxid txid msg)

但是我没有看到 ShareLock 是在哪里创建的,也没有看到为什么事务 4773 会在事务 4774 上阻塞。我在查询 pg_locks 表时看到类似的结果:始终等待另一个事务的 ShareLock 的事务,该事务阻塞在第一个事务的元组上。关于如何深入了解 ShareLock 的来源有什么建议吗?

更新 3:我需要更新 LOCK_DEBUG_ENABLED 内联函数以无条件返回 true 以查看 ShareLock 创建。一旦我这样做了,我就开始看到他们的创作:

2014-08-05 12:53:22 EDT apiary [76705] 2/471 6294 LOG:  00000: LockAcquire: lock [6294,0] ExclusiveLock
2014-08-05 12:53:22 EDT apiary [76705] 2/471 6294 LOG:  00000: LockAcquire: new: lock(0x115818378) id(6294,0,0,0,4,1) grantMask(0) req(0,0,0,0,0,0,0)=0 grant(0,0,0,0,0,0,0)=0 wait(0) type(ExclusiveLock)
2014-08-05 12:53:22 EDT apiary [76705] 2/471 6294 LOG:  00000: GrantLock: lock(0x115818378) id(6294,0,0,0,4,1) grantMask(80) req(0,0,0,0,0,0,1)=1 grant(0,0,0,0,0,0,1)=1 wait(0) type(ExclusiveLock)
2014-08-05 12:53:22 EDT apiary [76706] 4/153 6295 LOG:  00000: LockAcquire: lock [6294,0] ShareLock
2014-08-05 12:53:22 EDT apiary [76706] 4/153 6295 LOG:  00000: LockAcquire: found: lock(0x115818378) id(6294,0,0,0,4,1) grantMask(80) req(0,0,0,0,0,0,1)=1 grant(0,0,0,0,0,0,1)=1 wait(0) type(ShareLock)
2014-08-05 12:53:22 EDT apiary [76706] 4/153 6295 LOG:  00000: WaitOnLock: sleeping on lock: lock(0x115818378) id(6294,0,0,0,4,1) grantMask(80) req(0,0,0,0,1,0,1)=2 grant(0,0,0,0,0,0,1)=1 wait(0) type(ShareLock)

但我仍然不太确定为什么要创建 ShareLock,以及为什么 6295(在本例中)必须等待 6294。

最佳答案

这可能就是死锁发生的方式。每个表在用户表的 user_id 上都有一个外键。当您插入到具有外键约束的表中时,postgres 需要锁定引用表的行以确保在您插入引用它的行时它不会被删除(并违反提交时的 FK 约束)。现在这应该是一个共享锁。

看起来所有引用用户的表的插入/更新也在主表上插入后更新用户表上的用户序列。这些更新需要独占锁,并且会被不属于当前事务的任何共享锁阻塞。如果两者同时发生,它们就会陷入僵局。

例如,同时插入到 media_size 和 source 的两个事务可能会死锁,就像这样:

T1                                   T2
-----------------------------------------------------------------------
1. Insert media size
1a. Excl Lock media size row
1b. Shared Lock on user row (FK)
                                     2. Insert Source
                                     2a. Excl Lock source row
                                     2b. Shared lock on user row (FK)

3. Update user seq
3a. Excl Lock on user row (BLOCKS on 2b)
                                     4. Update user seq
                                     4a. Excl Lock on user row (Blocks on 1b)
5. Deadlock

我认为将更新用户序列步骤切换为第一个是有意义的,因为它会强制 T1 和 T2 在尝试获取共享锁之前阻塞(它不需要共享锁,因为它已经有一个排他锁)。

关于sql - 我怎样才能避免 Postgres 中的这种三向死锁?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25123079/

相关文章:

并发 INSERT 和 SELECT 导致的 MySQL 死锁

c - 死锁预防,随机线程执行顺序C

mysql - SQL 内连接是正确的方法吗?

mysql - Excel 无法将 mysql 中的日期识别为日期

postgresql - 我如何发现 PostgreSQL 数据库的结构?

sql - 使用选定时间段从数据库中的一列或两列中计算大量内容的有效方法

c# - 如何从 SqlDataReader.Read() 期间的死锁异常中恢复

sql - Oracle 10g 范围分区查询

mysql - 想要在变量中获取数据库值

node.js - 在同一个表中的多个列上更新 Postgresql