[英]Avoid exclusive access locks on referenced tables when DROPping in PostgreSQL
为什么删除 PostgreSQL 中的表需要对任何引用的表使用ACCESS EXCLUSIVE
锁? 如何将其减少为ACCESS SHARED
锁定或根本不锁定? 即有没有办法在不锁定引用表的情况下删除关系?
我在文档中找不到任何关于需要哪些锁的提及,但是除非我在并发操作期间删除多个表时以正确的顺序明确获取锁,否则我可以看到死锁在日志中等待 AccessExclusiveLock,并获取此限制当表被删除时,对常用表的锁定会导致其他进程的暂时延迟。
澄清,
CREATE TABLE base (
id SERIAL,
PRIMARY KEY (id)
);
CREATE TABLE main (
id SERIAL,
base_id INT,
PRIMARY KEY (id),
CONSTRAINT fk_main_base (base_id)
REFERENCES base (id)
ON DELETE CASCADE ON UPDATE CASCADE
);
DROP TABLE main; -- why does this need to lock base?
对于任何使用谷歌搜索并试图了解为什么他们的删除表(或删除外键或添加外键)长时间卡住的人:
PostgreSQL (我查看了 9.4 到 13 版)外键约束实际上是使用外键两端的触发器来实现的。
如果您有一个公司表(id 作为主键)和一个 bank_account 表(id 作为主键,company_id 作为指向 company.id 的外键),那么实际上在 bank_account 表上有 2 个触发器,在公司上也有 2 个触发器桌子。
表名 | 定时 | 触发器名称 | 函数名 |
---|---|---|---|
银行账户 | 更新后 | RI_ConstraintTrigger_c_1515961 | RI_FKey_check_upd |
银行账户 | 插入后 | RI_ConstraintTrigger_c_1515960 | RI_FKey_check_ins |
公司 | 更新后 | RI_ConstraintTrigger_a_1515959 | RI_FKey_noaction_upd |
公司 | 删除后 | RI_ConstraintTrigger_a_1515958 | RI_FKey_noaction_del |
这些触发器的初始创建(在创建前键时)需要在这些表上使用 SHARE ROW EXCLUSIVE 锁(它曾经是 9.4 版及更早版本中的 ACCESS EXCLUSIVE 锁)。 此锁与“数据读取锁”不冲突,但会与所有其他锁冲突,例如对公司表的简单 INSERT/UPDATE/DELETE。
删除这些触发器(删除外键或整个表时)需要在这些表上使用 ACCESS EXCLUSIVE 锁。 此锁与其他所有锁冲突!
所以想象一个场景,你有一个事务 A 运行,它首先从公司表中执行一个简单的 SELECT(导致它持有公司表的 ACCESS SHARE 锁,直到事务被提交或回滚),现在正在做一些其他的工作3分钟。 您尝试删除事务 B 中的 bank_account 表。这需要 ACCESS EXCLUSIVE 锁,这需要等到 ACCESS SHARE 锁先被释放。 除此之外,所有其他想要访问公司表的事务(只是 SELECT,或者可能是 INSERT/UPDATE/DELETE),将排队等待 ACCESS EXCLUSIVE 锁,它正在等待 ACCESS SHARE 锁。
长时间运行的事务和 DDL 更改需要精细处理。
-- SESSION#1
DROP SCHEMA tmp CASCADE;
CREATE SCHEMA tmp ;
SET search_path=tmp;
BEGIN;
CREATE TABLE base (
id SERIAL
, dummy INTEGER
, PRIMARY KEY (id)
);
CREATE TABLE main (
id SERIAL
, base_id INTEGER
, PRIMARY KEY (id)
, CONSTRAINT fk_main_base FOREIGN KEY (base_id) REFERENCES base (id)
-- comment the next line out ( plus maybe tghe previous one)
ON DELETE CASCADE ON UPDATE CASCADE
);
-- make some data ...
INSERT INTO base (dummy)
SELECT generate_series(1,10)
;
-- make some FK references
INSERT INTO main(base_id)
SELECT id FROM base
WHERE random() < 0.5
;
COMMIT;
BEGIN;
DROP TABLE main; -- why does this need to lock base?
SELECT pg_backend_pid();
-- allow other session to check the locks
-- and attempt an update to "base"
SELECT pg_sleep(20);
-- On rollback the other session will fail.
-- On commit the other session will succeed.
-- In both cases the other session must wait for us to complete.
-- ROLLBACK;
COMMIT;
-- SESSION#2
-- (Start this after session#1 from a different terminal)
SET search_path = tmp, pg_catalog;
PREPARE peeklock(text) AS
SELECT dat.datname
, rel.relname as relrelname
, cat.relname as catrelname
, lck.locktype
-- , lck.database, lck.relation
, lck.page, lck.tuple
-- , lck.virtualxid, lck.transactionid
-- , lck.classid
, lck.objid, lck.objsubid
-- , lck.virtualtransaction
, lck.pid, lck.mode, lck.granted, lck.fastpath
FROM pg_locks lck
LEFT JOIN pg_database dat ON dat.oid = lck.database
LEFT JOIN pg_class rel ON rel.oid = lck.relation
LEFT JOIN pg_class cat ON cat.oid = lck.classid
WHERE EXISTS(
SELECT * FROM pg_locks l
JOIN pg_class c ON c.oid = l.relation AND c.relname = $1
WHERE l.pid =lck.pid
)
;
EXECUTE peeklock( 'base' );
BEGIN;
-- attempt to perfom some DDL
ALTER TABLE base ALTER COLUMN id TYPE BIGINT;
-- attempt to perfom some DML
UPDATE base SET id = id+100;
COMMIT;
EXECUTE peeklock( 'base' );
\d base
SELECT * FROM base;
我想 DDL 只为了简单起见锁定它接触的所有东西——无论如何,你不应该在正常操作期间运行涉及非临时表的 DDL。
为了避免死锁,您可以使用咨询锁:
start transaction;
select pg_advisory_xact_lock(0);
drop table main;
commit;
这将确保只有一个客户端同时运行涉及引用表的 DDL,因此获取其他锁的顺序无关紧要。
您可以通过先删除外键来避免长时间锁定表:
start transaction;
select pg_advisory_xact_lock(0);
alter table main drop constraint fk_main_base;
commit;
start transaction;
drop table main;
commit;
这仍然需要以独占方式锁定base
,但时间要短得多。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.