2014-10-20 48 views
0

比方说,我有一个交易表和transaction_summary表。我创建了以下触发器以更新transaction_summary表。锁定在Postgres功能

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 

看来只有选项是每次在交易开始时锁定整个表。

+0

有你尝试将'transaction_summary'连接到您的锁定查询中,并应用'FOR UPDATE的transaction_summary'? (如果你在'transaction_summary'中有明确定义的主键,你可以保留整个表的锁定值) - 另外,也可以使用'FOUND'特殊变量来代替'rec_cnt'变量。 – pozs 2014-10-20 08:24:30

+2

另外,你想用它达到什么目的?也许一个多线程安全UPSERT? http://stackoverflow.com/questions/17267417/how-do-i-do-an-upsert-merge-insert-on-duplicate-update-in-postgresql – pozs 2014-10-20 08:29:07

+0

@Pozs - 是的,我想要得到的是一个线程安全_upsert_操作。一个插入_transaction table_可以在_transaction_summary_表上导致两个_upserts_。 PS:谢谢,我没有意识到有一个特殊的变量“FOUND” – robert 2014-10-20 08:47:29

回答

2

您的'SELECT 1 FROM WHERE ...'是为了访问'transactions_summary'吗?此外,请注意,如果两个DB事务插入两个“事务”行,那么这两个查询至少在理论上可能会相互死锁,其中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,user2)FOR UPDATE'。如果有人忘记,它会破坏,但你的表锁定也会破坏。通过这样做,您可以为多个单独的行锁交换一个大表锁。
  • 为transactions_summary添加UNIQUE约束,并在违规时查找错误。无论如何,您应该添加约束条件,即使您以其他方式处理。它会检测到错误。
  • 您可以允许重复的transaction_summary行,并要求该表的用户将其添加。凌乱,对于不知道创建错误的开发者来说很容易(尽管你可以添加一个添加视图)。但是,如果你真的无法承受锁定和僵局的表现,你可以做到。
  • 您可以尝试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. 
+0

是的,它必须是'SELECT 1 FROM transaction_summary WHERE ...'我简化了我的真实工作代码并犯了一个错误:) – robert 2014-10-20 18:51:19

+0

+ 1为好的例子,我将它标记为接受,因为你回答了我关于死锁的问题 – robert 2014-11-05 21:10:18