如何通过更新操作本身维护来自触发器 "before update" 的 postgreSQL 锁
How to maintain a postgreSQL lock from a trigger "before update" through the update operation itself
如果这是一个已回答的问题,我深表歉意,我做了一些研究,但找不到答案。
我在我的代码中维护了一个类似于 folder/file 的结构,我在其中订购了在更新和删除操作时级联顺序更改的项目。但是,这些触发器既需要锁定行以确保顺序更改完成,又需要继续锁定直到操作完成
更新过程比较简单。这是整个操作的管理伪代码:
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 都改变了彼此在列表中的位置,但是如果在状态改变发生之前,前操作触发了每个 运行s,那么它们相互执行的操作将被丢弃。
是否有一种直接的方法来确保从开始到结束这些 tables 都保持锁定,或者以其他方式修复这种竞争条件?我一直在考虑创建一个单独的 table 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
。
如果这是一个已回答的问题,我深表歉意,我做了一些研究,但找不到答案。
我在我的代码中维护了一个类似于 folder/file 的结构,我在其中订购了在更新和删除操作时级联顺序更改的项目。但是,这些触发器既需要锁定行以确保顺序更改完成,又需要继续锁定直到操作完成
更新过程比较简单。这是整个操作的管理伪代码:
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 都改变了彼此在列表中的位置,但是如果在状态改变发生之前,前操作触发了每个 运行s,那么它们相互执行的操作将被丢弃。
是否有一种直接的方法来确保从开始到结束这些 tables 都保持锁定,或者以其他方式修复这种竞争条件?我一直在考虑创建一个单独的 table 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
。