10

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

3 回答 3

5

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

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

如果你有一个 company 表(id​​ 作为主键)和一个 bank_account 表(id​​ 作为主键,company_id 作为指向 company.id 的外键),那么实际上 bank_account 表上有 2 个触发器,company 上有 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 正在运行,它首先从 company 表中执行了一个简单的 SELECT (导致它为 company 表持有一个 ACCESS SHARE 锁,直到事务被提交或回滚),现在正在做一些其他的工作3分钟。您尝试删除事务 B 中的 bank_account 表。这需要 ACCESS EXCLUSIVE 锁,这需要等到 ACCESS SHARE 锁首先被释放。除此之外,所有其他想要访问公司表的事务(只是 SELECT,或者可能是 INSERT/UPDATE/DELETE)将排队等待 ACCESS EXCLUSIVE 锁,该锁正在等待 ACCESS SHARE 锁。

长时间运行的事务和 DDL 更改需要精细处理。

于 2021-01-21T16:05:28.317 回答
0

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

于 2015-08-22T20:46:34.260 回答
0
        -- 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;
于 2015-08-22T19:40:21.340 回答