在 PostgreSQL 中进行 DROPping 时避免对引用表的独占访问锁

Posted

技术标签:

【中文标题】在 PostgreSQL 中进行 DROPping 时避免对引用表的独占访问锁【英文标题】:Avoid exclusive access locks on referenced tables when DROPping in PostgreSQL 【发布时间】:2015-08-21 16:26:20 【问题描述】:

为什么在 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?

【问题讨论】:

您希望发生什么?您正试图从一家繁忙的餐厅搬走家具,人们还在用餐中途,他们应该支付什么费用?半顿饭?全餐?只有他们消耗的部分? @wildplasser 我不明白你的类比;我不是在谈论我要删除的表上的锁,而是在它引用的表上。被引用的表不应该关心是否有删除 - 它的数据不会被操作更改。它根本不需要做任何事情。 @wildplasser 看我的例子 我明白了。我仍然不清楚为什么你想删除和(也许)重新创建表。顺便说一句:目录上的酸很难;可能是 postgres 在这里过于防守了。 @wildplasser 表没有被重新创建。这些表用于确保客户隔离(例如 project_1_data 等)。我们正在迁移到为此使用模式(我们可以简单地复制共享信息),但由于我们仍在从 mysql 迁移的过程中,我们还不能做出改变。当我们归档死项目时,表格会被删除,但更重要的是在集成测试期间,我们会快速创建和销毁项目以确保其测试环境的隔离。 【参考方案1】:

对于任何在谷歌上搜索并试图了解为什么他们的删除表(或删除外键或添加外键)长时间卡住的人:

PostgreSQL(我查看了 9.4 到 13 版本)外键约束实际上是使用外键两端的触发器实现的

如果你有一个 company 表(id​​ 作为主键)和一个 bank_account 表(id​​ 作为主键,company_id 作为指向 company.id 的外键),那么实际上 bank_account 表上有 2 个触发器和 2 个触发器在公司的桌子上。

table_name timing trigger_name function_name
bank_account AFTER UPDATE RI_ConstraintTrigger_c_1515961 RI_FKey_check_upd
bank_account AFTER INSERT RI_ConstraintTrigger_c_1515960 RI_FKey_check_ins
company AFTER UPDATE RI_ConstraintTrigger_a_1515959 RI_FKey_noaction_upd
company AFTER DELETE 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 更改需要精细处理。

【讨论】:

请不要在多个问题中添加the same answer。回答最好的一个并将其余的标记为重复。见Is it acceptable to add a duplicate answer to several questions? 它不会让我将另一个标记为重复,因为这里的这个还没有一个可接受的答案。但我从另一个问题中删除了我的答案。 现在它有一个可以接受的答案 ;) – 这是很好的信息;感谢您回答一个 6 岁的问题!【参考方案2】:
        -- 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;

【讨论】:

“回滚时另一个会话将失败”:我认为这是一种对您来说显而易见的可能性,但我不知何故忽略了:DROP TABLE 事务可以回滚。这就解释了为什么不能立即删除和忘记约束。不过,我仍然不确定锁的类型; ALTER TABLEUPDATE 是需要锁的好例子,但我相信两者都可以通过 ACCESS SHARED 锁来避免。有没有你能想到 ACCESS SHARED 不够用的例子,或者只是你最初建议的 postgres 过于防御? 经验法则:不要混合 DDL 和 DML。 (IOW:当你在做 DDL 时踢掉其他会话。出于测试目的,这不会导致问题)是的:这种课程锁定过于保守;有原因的。【参考方案3】:

我想为了简单起见,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,但时间要短得多。

【讨论】:

短了多少? X秒有限制吗?还是只缩短 10%?我之所以这么问,是因为知道能够在大型基础上在线进行与不进行多长时间的区别。

以上是关于在 PostgreSQL 中进行 DROPping 时避免对引用表的独占访问锁的主要内容,如果未能解决你的问题,请参考以下文章

[POJ2976] Dropping tests

UVa 679. Dropping Balls

Dropping TSO features since no CSUM feature

由于 SSL 连接已经在喷雾中关闭错误,如何修复 Dropping Close

Dropping tests

Uva679 Dropping Balls