为什么这种外键+主键的组合会产生 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;
执行顺序为:
- 线程 1:开始
- 线程 2:开始
- 线程 1:锁定
c
- 线程 2:插入
e
- 线程 1:插入
e
<-- 死锁
为什么会这样?
在线程 2 上给 c
添加锁当然可以避免死锁,但我不清楚为什么。同样有趣的是,如果 e
中的行存在于线程 1 或 2 运行 之前,则不会发生死锁。
我怀疑至少有两件事正在发生:
- 主键创建了一个唯一约束,导致对
e
的某种我不理解的锁定,即使使用 ON CONFLICT DO NOTHING
.
c_id
上的外键会导致某种触发器,导致在插入新记录时锁定 c
(我想是在更新 c_id
时)。
谢谢!
为了保持完整性,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 是提交还是回滚。所以在我们知道是否会发生冲突之前就发生了死锁。
从这两个表开始,这是 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;
执行顺序为:
- 线程 1:开始
- 线程 2:开始
- 线程 1:锁定
c
- 线程 2:插入
e
- 线程 1:插入
e
<-- 死锁
为什么会这样?
在线程 2 上给 c
添加锁当然可以避免死锁,但我不清楚为什么。同样有趣的是,如果 e
中的行存在于线程 1 或 2 运行 之前,则不会发生死锁。
我怀疑至少有两件事正在发生:
- 主键创建了一个唯一约束,导致对
e
的某种我不理解的锁定,即使使用ON CONFLICT DO NOTHING
. c_id
上的外键会导致某种触发器,导致在插入新记录时锁定c
(我想是在更新c_id
时)。
谢谢!
为了保持完整性,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 是提交还是回滚。所以在我们知道是否会发生冲突之前就发生了死锁。