postgresql - 如何通过更新操作本身维护来自触发器 "before update"的 postgreSQL 锁

标签 postgresql plpgsql race-condition database-trigger

如果这是一个已回答的问题,我深表歉意,我做了一些研究,但找不到答案。

我在我的代码中维护一个类似文件夹/文件的结构,我在其中订购了在更新和删除操作时级联顺序更改的项目。但是,这些触发器既需要锁定行以确保顺序更改完成,又需要通过操作完成继续锁定

更新过程比较简单。这是整个操作的管理伪代码:

check if pg_trigger_depth() >= 1
    return because this was a cascaded update from a trigger

lock the table for update on items with the old folder_parent_id
lock the table for update on items with the new folder_parent_id

update the old rows setting order_number -= 1 where the order_number is > the old order_number, and the folder_parent_id is the same as the old one
update the new rows setting order_number +=1 where the order_number is >= the new order_number and the folder_parent_id is the same as the new one

allow the update operation to go through (setting the order_number/folder_parent_id of this row to its new location)


release the lock for update on items with the old folder_parent_id
release the lock for update on items with the new folder_parent_id

如果锁在实际操作完成之前被释放,这种竞争条件就会发生。在此示例问题中,同时调用了两个更新:

给定文件夹的子级:a(0)、b(1)、c(2)、d(3)、e(4)

字母是标识属性,数字是订单号

我们要运行这些操作:c(2 -> 1), d(3 -> 0)

以下是这些操作的时间表:

更新 c 之前:

decrement everything > OLD c.order_number (d--, e--)
increment everything >= NEW c.order_number (b++, d++, e++)

当前状态:a(0), b(2), c(2), d(3), e(4)

更新日期之前:

decrement everything > OLD d.order_number (e--)
increment everything > NEW d.order_number (a++, b++, c++, e++)

当前状态:a(1), b(3), c(3), d(3), e(4)

设置 c = 1

设置 d = 0

最终状态:d(0), a(1), c(1), b(3), e(4)

显然,这里的竞争条件是 c 和 d 都改变彼此在列表中的位置这一事实,但是如果在状态改变发生之前每个前操作触发器都运行,那么它们对彼此执行的操作是丢弃。

是否有一种直接的方法来确保从该操作的开始到结束都在这些表上维护锁,或者以其他方式修复这种竞争条件?我一直在考虑创建一个单独的表 File_Structure_Lock,它将在前触发器中锁定以进行更新,然后在后触发器中解锁以绕过 PostgreSQL 锁定系统,但我认为必须有更好的方法。

编辑:我被要求提供实际的 SQL。我这里的问题是准备对已经存在的代码进行重构,因为该代码具有导致错误的竞争条件。我可以尝试在一分钟内将其标记出来,但这是我正在使用的原始代码,更改了一些变量名称以使其更易于理解

CREATE OR REPLACE FUNCTION getOrderLock() RETURNS TRIGGER AS $getOrderLock$
BEGIN
    PERFORM * FROM Folders FOR UPDATE;
    PERFORM * FROM Files FOR UPDATE;
    IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
        RETURN NEW;
        ELSIF (TG_OP = 'DELETE') THEN
        RETURN OLD;
    END IF;
END;
$getOrderLock$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_folder_lock_rows
BEFORE INSERT OR UPDATE OR DELETE ON Folders
FOR EACH STATEMENT
WHEN (pg_trigger_depth() < 1)
EXECUTE PROCEDURE getOrderLock();

CREATE TRIGGER trigger_file_lock_rows
BEFORE INSERT OR UPDATE OR DELETE ON Files
FOR EACH STATEMENT
WHEN (pg_trigger_depth() < 1)
EXECUTE PROCEDURE getOrderLock();


CREATE OR REPLACE FUNCTION adjust_order_numbers_after_folder_update() RETURNS TRIGGER AS $adjust_order_numbers_after_nav_update$
BEGIN
    --update old location
    UPDATE Folders
        SET order_number = Folders.order_number - 1
        WHERE Folders.order_number >= OLD.order_number
        AND Folders.page_id = OLD.page_id
        AND COALESCE(Folders.folder_parent_id, 0) = COALESCE(OLD.folder_parent_id, 0)
        AND Folders.id != NEW.id;

    UPDATE Files
        SET order_number = Files.order_number - 1
        WHERE Files.order_number >= OLD.order_number
        AND Files.page_id = OLD.page_id
        AND COALESCE(Files.folder_parent_id, 0) = COALESCE(OLD.folder_parent_id, 0);

    --update new location
    UPDATE Folders
        SET order_number = Folders.order_number + 1
        WHERE Folders.order_number >= NEW.order_number
        AND Folders.page_id = NEW.page_id
        AND COALESCE(Folders.folder_parent_id, 0) = COALESCE(NEW.folder_parent_id, 0)
        AND Folders.id != NEW.id;

    UPDATE Files
        SET order_number = Files.order_number + 1
        WHERE Files.order_number >= NEW.order_number
        AND Files.page_id = NEW.page_id
        AND COALESCE(Files.folder_parent_id, 0) = COALESCE(NEW.folder_parent_id, 0);

    RETURN NEW;
END;
$adjust_order_numbers_after_nav_update$ LANGUAGE plpgsql;


CREATE OR REPLACE FUNCTION adjust_order_numbers_after_file_update() RETURNS TRIGGER AS $adjust_order_numbers_after_file_update$
BEGIN

    --update old location
    UPDATE Folders
        SET order_number = Folders.order_number - 1
        WHERE Folders.order_number >= OLD.order_number
        AND Folders.page_id = OLD.page_id
        AND COALESCE(Folders.folder_parent_id, 0) = COALESCE(OLD.folder_parent_id, 0);

    UPDATE Files
        SET order_number = Files.order_number - 1
        WHERE Files.order_number >= OLD.order_number
        AND Files.page_id = OLD.page_id
        AND COALESCE(Files.folder_parent_id, 0) = COALESCE(OLD.folder_parent_id, 0)
        AND Files.id != NEW.id;

    --update new location
    UPDATE Folders
        SET order_number = Folders.order_number + 1
        WHERE Folders.order_number >= NEW.order_number
        AND Folders.page_id = NEW.page_id
        AND COALESCE(Folders.folder_parent_id, 0) = COALESCE(NEW.folder_parent_id, 0);

    UPDATE Files
        SET order_number = Files.order_number + 1
        WHERE Files.order_number >= NEW.order_number
        AND Files.page_id = NEW.page_id
        AND COALESCE(Files.folder_parent_id, 0) = COALESCE(NEW.folder_parent_id, 0)
        AND Files.id != NEW.id;

    RETURN NEW;
END;
$adjust_order_numbers_after_file_update$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_folder_order_shift
AFTER UPDATE ON Folders
FOR EACH ROW 
WHEN (
    (
        COALESCE(OLD.folder_parent_id, 0) != COALESCE(NEW.folder_parent_id, 0)
        OR OLD.order_number != NEW.order_number
        OR Old.page_id != New.page_id
    )
    AND pg_trigger_depth() < 1
)
EXECUTE PROCEDURE adjust_order_numbers_after_folder_update();

CREATE TRIGGER trigger_file_order_shift
AFTER UPDATE ON Files
FOR EACH ROW 
WHEN (
    (
        COALESCE(OLD.folder_parent_id, 0) != COALESCE(NEW.folder_parent_id, 0)
        OR OLD.order_number != NEW.order_number
        OR Old.page_id != New.page_id
    )
    AND pg_trigger_depth() < 1
)
EXECUTE PROCEDURE adjust_order_numbers_after_file_update();

最佳答案

问题似乎出在 order_number 上,您坚持认为它是一个无间隙的整数序列,用于对每个文件夹中的项目进行排序。如果你想保持这一点,你必须洗牌所有项目,如果没有一些主要的锁定,确实很难做到这一点。

但是如果你只想保持项目的特定顺序,我会放宽无间隙序列的要求,而是使用 double precision 值来描述项目的顺序.这样就可以很容易地在任何其他元素中插入一个项目,而无需更改任何其他元素中的 order_number - 您始终可以为移动的项目分配一个介于任意两个现有项目之间的 order_number

关于postgresql - 如何通过更新操作本身维护来自触发器 "before update"的 postgreSQL 锁,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52486021/

相关文章:

c# - 异步/等待代码中的竞争条件

postgresql - 如何将非空间表连接到 Postgis 数据库中的空间表(两个表都在不同的数据库中)

sql - 设计可以引用多个其他表的数据库表

arrays - 作为存储过程输入的复合类型数组

sql - 将 SQL 结果拆分为最大大小 = n 的组

c - 在 libev 中锁定 io watcher 和 timer watch

postgresql查询这种情况?

postgresql - 仅使用 SELECT 从 information_schema 查找 Postgresql 中表的主键

arrays - 在 plpgsql 中循环遍历数组维度

java - 原始数据类型在 Java 中是原子的