假设我有一个transactions 表和transaction_summary 表。我创建了以下触发器来更新 transaction_summary 表。
CREATE OR REPLACE FUNCTION doSomeThing() RETURNS TRIGGER AS
$BODY$
DECLARE
rec_cnt bigint;
BEGIN
-- lock rows which have to be updated
SELECT count(1) from (SELECT 1 FROM transaction_summary WHERE receiver = new.receiver FOR UPDATE) r INTO rec_cnt ;
IF rec_cnt = 0
THEN
-- if there are no rows then create new entry in summary table
-- lock whole table
LOCK TABLE "transaction_summary" IN ACCESS EXCLUSIVE MODE;
INSERT INTO transaction_summary( ... ) VALUES ( ... );
ELSE
UPDATE transaction_summary SET ... WHERE receiver = new.receiver;
END IF;
SELECT count(1) from (SELECT 1 FROM transaction_summary WHERE sender = new.sender FOR UPDATE) r INTO rec_cnt ;
IF rec_cnt = 0
THEN
LOCK TABLE "transaction_summary" IN ACCESS EXCLUSIVE MODE;
INSERT INTO transaction_summary( ... ) VALUES ( ... );
ELSE
UPDATE transaction_summary SET ... WHERE sender = new.sender;
END IF;
RETURN new;
END;
$BODY$
language plpgsql;
问题:会出现死锁吗?根据我的理解,死锁可能会发生这样的情况:
_________
|__table__| <- executor #1 waits on executor #2 to be able to lock the whole table AND
|_________| executor #2 waits on executor #1 to be able to lock the whole table
|_________|
|_________| <- row is locked by executor #1
|_________|
|_________| <- row is locked by executor #2
似乎唯一的选择是每次在事务开始时锁定整个表。
最佳答案
您的“SELECT 1 FROM transactions WHERE ...”是否意味着访问“transactions_summary”?另请注意,如果两个数据库事务插入两个“事务”行(new.sender1=new.receiver2 和 new.receiver1=new.sender2),那么这两个查询至少在理论上会彼此死锁。
一般来说,您不能保证数据库不会出现死锁。即使您尝试通过仔细编写查询(例如,排序更新)来阻止它们,您仍然可能会陷入困境,因为您无法控制 INSERT/UPDATE 或约束检查的顺序。无论如何,将每个事务与其他事务进行比较以检查死锁并不会随着应用程序的增长而扩展。
因此,当您收到“检测到死锁”错误时,您的代码应该始终准备好重新运行事务。如果您这样做并且认为冲突事务并不常见,那么您不妨让死锁处理代码来处理它。
如果您认为死锁很常见,那么它可能会导致性能问题 - 尽管争用大表锁也可能是这样。以下是一些选项:
- 例如,如果 new.receiver 和 new.sender 是 MyUsers 表中的行 ID,则可能需要插入“transactions_summary”的所有代码首先执行“SELECT 1 FROM MyUsers WHERE id IN (user1,用户2)用于更新'。如果有人忘记,它就会损坏,但你的表锁定也会损坏。通过这种方式,您可以将一个大表锁替换为许多单独的行锁。
- 将 UNIQUE 约束添加到 transactions_summary 中,并在违反时查找错误。无论如何,你可能应该添加约束,即使你以另一种方式处理这个问题。它会检测错误。
- 您可以允许重复的 transaction_summary 行,并要求该表的用户将它们相加。困惑,并且对于不知道创建错误的开发人员来说很容易(尽管您可以添加一个执行添加操作的 View )。但如果您确实无法承受锁定和死锁对性能的影响,您也可以这样做。
- 您可以尝试 SERIALIZABLE 事务隔离级别并取出表锁。根据我的阅读,SELECT ... FOR UPDATE 应该创建一个谓词锁(普通的 SELECT 也应该如此)。这将阻止任何其他进行冲突插入的事务成功提交。但是,在整个应用程序中使用 SERIALIZABLE 会降低性能,并需要重试更多事务。
以下是 SERIALIZABLE 事务隔离级别的工作原理:
create table test (id serial, x integer, total integer); ...
交易1:
DB=# begin transaction isolation level serializable;
BEGIN
DB=# insert into test (x, total) select 3, 100 where not exists (select true from test where x=3);
INSERT 0 1
DB=# select * from test;
id | x | total
----+---+-------
1 | 3 | 100
(1 row)
DB=# commit;
COMMIT
事务 2,与第一行交错行:
DB=# begin transaction isolation level serializable;
BEGIN
DB=# insert into test (x, total) select 3, 200 where not exists (select true from test where x=3);
INSERT 0 1
DB=# select * from test;
id | x | total
----+---+-------
2 | 3 | 200
(1 row)
DB=# commit;
ERROR: could not serialize access due to read/write dependencies among transactions
DETAIL: Reason code: Canceled on identification as a pivot, during commit attempt.
HINT: The transaction might succeed if retried.
关于postgresql - Postgres 函数中的锁定,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/26460285/