2014-10-20 52 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