涉及外键约束的死锁

时间:2013-08-30 15:06:59

标签: postgresql deadlock database-deadlocks

我想更好地理解postgres中的锁定机制。

让我们说树可以有苹果(通过苹果桌上的外键)。似乎在选择树时进行更新锁定是在苹果上获得的。但是,即使其他人已经锁定了这个苹果,操作也不会被阻止。

为什么会这样?

P.S。请勿建议删除"选择更新"。

方案

Transaction 1      Transaction 2
BEGIN              .
update apple;      .
.                  BEGIN
.                  select tree for update;
.                  update apple;
.                  --halts because of the other transaction locking an apple
update apple;      .
-- deadlock        .
                   COMMIT
                   --transaction succeeds

代码

如果你想在你的postgres中尝试 - 这是一个你可以复制/粘贴的代码。

我有以下数据库架构

CREATE TABLE trees (
    id       integer primary key
);

create table apples (
    id       integer primary key,
    tree_id  integer references trees(id)
);

和非常简单的数据

insert into trees values(1);
insert into apples values(1,1);

有两个简单的交易。一个是更新苹果,第二个是锁定树并更新苹果。

BEGIN;
    UPDATE apples SET id = id WHERE id = 1;
    -- run second transaction in paralell
    UPDATE apples SET id = id WHERE id = 1;
COMMIT;

BEGIN;
    SELECT id FROM trees WHERE id = 1 FOR UPDATE;
    UPDATE apples SET id = id WHERE id = 1;
COMMIT;

当我运行它们时 - 在第一次交易的第二次更新时发生死锁。

ERROR:  deadlock detected
DETAIL:  Process 81122 waits for ShareLock on transaction 227154; blocked by process 81100.
Process 81100 waits for ShareLock on transaction 227153; blocked by process 81122.
CONTEXT:  SQL statement "SELECT 1 FROM ONLY "public"."trees" x WHERE "id" OPERATOR(pg_catalog.=) $1 FOR SHARE OF x"

2 个答案:

答案 0 :(得分:9)

只是一个疯狂的猜测:你遇到了与实现细节相关的问题......

具体来说,您的select tree for update语句获取树上的独占锁。并且update apples语句获得对相关苹果的独占锁定。

当您在Apple上运行更新时,Postgres的每行相关的外键触发会触发,以确保tree_id存在。我不记得他们的精确名字在我的头顶,但它们在目录中,并且文档中有一些零碎的部分明确地或隐含地引用它们,例如:

create constraint trigger ... on ... from ...

http://www.postgresql.org/docs/current/static/sql-createtrigger.html

无论如何,这些触发器将运行以下内容:

select exists (select 1 from trees where id = 1);

这就是你的问题所在:由select for update引起的独占访问使得它等待事务2释放树上的锁,以便最终确定其对apple的更新语句,但事务2正在等待事务1完成以获得对苹果的锁定,以便开始关于苹果的更新声明。

结果,Postgres陷入僵局。

答案 1 :(得分:-3)

看起来索引锁在整个事务期间都没有保留。我认为主要问题是事务1执行相同的UPDATE两次,但它需要获取更多锁来执行第二次UPDATE

根据docs,索引锁只能保持很短的时间。与数据锁不同,它们在事务完成之前不会被保留。让我们更详细地看一下时间表。

事务1执行第一个UPDATE。这会在apples中的行上获取行级锁定。在操作期间,它还获取trees中索引的锁定。事务尚未提交,因此行级数据锁仍由转换1保留。但是,trees上的索引锁定会立即释放。不确定为什么Postgres会为所有索引类型执行此操作。

事务2出现并锁定trees以进行更新。这会锁定数据和索引。这不会阻止,因为事务1已经释放了索引锁。这一次,两个锁都保持到事务结束。不知道为什么在释放另一个索引时保持这个索引锁定。

交易1返回并再次尝试UPDATE。锁定apples很好,因为它已经拥有它。但是,trees上的锁定会阻止,因为事务2已经存在。

在事务2中添加UPDATE使其在事务1上等待,从而导致死锁。

修改

我现在回来调查一下,我已经安装了Postgres。这实际上很奇怪。在提交事务2之后我查看了pg_locks

交易1具有以下锁定:

  • 关于apples_pkey和apples的RowExclusive
  • 独占于其transactionid和virtualxid

事务2具有以下锁(以及许多其他不相关的锁):

  • 在trees_pkey上的AccessShare
  • 树上的RowShare
  • 独占于其transactionid和virtualxid
  • 关于apples_pkey和apples的RowExclusive
  • 独家使用苹果中的元组

交易2也在等待在交易1上获得股票锁定。

有趣的是,two transactions can hold a RowExclusive lock on the same table。但是,Exclusive锁与Share冲突,因此Transaction 2正在等待Transaction 1的事务ID。 docs提及事务锁定作为等待另一个事务的方式。因此,看起来交易2虽然已经提交,但仍在等待交易1。

当事务1继续时,它想要在事务2上获取共享锁,这会产生死锁。为什么要在Transaction 2上获得共享锁?对此不太确定。 The docs提示pg_locks中未提供此信息。我猜这与MVCC有关,但对我来说仍然是一个谜。