为什么这种外键+主键的组合会产生 Postgres 死锁?

Why does this combination of foreign + primary key produce a Postgres deadlock?

从这两个表开始,这是 c 的初始记录:

create table c
(
    id   serial primary key,
    name varchar not null
);

create table e
(
    id   varchar                  not null,
    c_id bigint references c (id) not null,
    name varchar                  not null,
    primary key (id, c_id)
);

insert into c (name) values ('deadlock test');

线程 1:

begin;
select * from c where id = 1 for update;
insert into e (id, c_id, name) VALUES ('bar', 1, 'second') on conflict do nothing ;
commit;

线程 2:

begin;
insert into e (id, c_id, name) VALUES ('bar', 1, 'first') on conflict do nothing ;
commit;

执行顺序为:

为什么会这样?

在线程 2 上给 c 添加锁当然可以避免死锁,但我不清楚为什么。同样有趣的是,如果 e 中的行存在于线程 1 或 2 运行 之前,则不会发生死锁。

我怀疑至少有两件事正在发生:

谢谢!

为了保持完整性,e 上的每个插入都将使用 KEY SHARE 锁锁定 c 中引用的行。这可以防止任何并发事务删除 c 中的行或修改主键。

这样的 KEY SHARE 锁与会话 1 显式占用的 UPDATE 锁冲突(参见 the documentation),因此会话 2 的 INSERT 阻塞 - 但它已经在 e.

的主键索引中插入(并锁定)一个索引元组

现在session 1想插入一个和session 2插入的主键相同的行,所以它会阻塞在session 2刚刚拿走的锁上,完美死锁。

您可能想知道为什么 ON CONFLICT DO NOTHING 不改变行为。但是 PostgreSQL 没有到达那里,因为要知道是否存在冲突,会话 1 必须等到它知道会话 2 是提交还是回滚。所以在我们知道是否会发生冲突之前就发生了死锁。