2013-10-11 126 views
2

問:
更新時間: 爲什麼插入一個行table A與外鍵約束table B,然後更新行table B的是,在交易table A引用插入的行造成僵局?

PSQL JDBC事務導致死鎖

場景:

  • reservation.time_slot_id有一個外鍵約束time_slot.id
  • 保留時提出了以下SQL運行:

    BEGIN TRANSACTION 
    INSERT INTO reservations (..., time_slot_id) VALUES (..., $timeSlotID) 
    UPDATE reservations SET num_reservations = 5 WHERE id = $timeSlotID 
    COMMIT 
    
  • 我的負載測試我的服務器有大約100個併發用戶,同一時隙(相同$timeSlotID每個各進行預訂用戶)。

  • 如果我不使用交易(刪除cn.setAutoCommit(false);,cn.commit()等),則不會發生此問題。


環境:

  • 的PostgreSQL 9.2.4
  • Tomcat的V7.0
  • JDK 1.7.0_40
  • 公地DBCP-1.4.jar
  • commons-pool-1.6.jar
  • PostgreSQL相關9.2-1002.jdbc4.jar


代碼:

// endpoint start 
// there are some other SELECT ... LEFT JOIN ... WHERE ... queries up here but they don't seem to be related 
... 

// create a reservation in the time slot then increment the count 

cn.setAutoCommit(false); 
try 
{ 
    st = cn.prepareStatement("INSERT INTO reservation (time_slot_id, email, created_timestamp) VALUES (?, ?, ?)"); 
    st.setInt (1, timeSlotID); // timeSlotID is the same for every user 
    st.setString(2, email); 
    st.setInt (3, currentTimestamp); 

    st.executeUpdate(); 
    st.close(); 

    st = cn.prepareStatement("UPDATE time_slot SET num_reservations = 5 WHERE id = ?"); // set to 5 instead of incrementing for testing 
    st.setInt(1, timeSlotID); // timeSlotID is the same for every user 

    st.executeUpdate(); 
    st.close(); 

    cn.commit(); 
} 
catch (SQLException e) 
{ 
    cn.rollback(); 
    ... 
} 
finally 
{ 
    cn.setAutoCommit(true); 
} 

... 
// endpoint end 


PSQL錯誤:

ERROR: deadlock detected 
DETAIL: Process 27776 waits for ExclusiveLock on tuple (2,179) of relation 49817 of database 49772; blocked by process 27795. 
    Process 27795 waits for ShareLock on transaction 3962; blocked by process 27777. 
    Process 27777 waits for ExclusiveLock on tuple (2,179) of relation 49817 of database 49772; blocked by process 27776. 
    Process 27776: UPDATE time_slot SET num_reservations = 5 WHERE id = $1 
    Process 27795: UPDATE time_slot SET num_reservations = 5 WHERE id = $1 
    Process 27777: UPDATE time_slot SET num_reservations = 5 WHERE id = $1 
HINT: See server log for query details. 
STATEMENT: UPDATE time_slot SET num_reservations = 5 WHERE id = $1 
+0

'timeSlotID'的值是什麼,他們很可能試圖更新相同的記錄。 –

+0

你指的是「他們」? try塊內的代碼是完整的代碼。因此,'timeSlotID'對於兩個查詢都是相同的值。 如果「他們」是來自負載測試的用戶會話,那麼你是正確的。現在回顧我的負載測試;隨機'timeSlotId'不是隨機的,並且爲每個「用戶」選擇相同的'timeSlotId'。 所以是的,每個「用戶」都試圖同時更新同一個'time_slot'行。但是爲什麼只有在使用事務時才存在這個問題? – Mike

+0

我看到了,但是你正在執行100個併發會話,如果其中任何會話使用相同的'timeSlotID',可能會導致死鎖 –

回答

4

外鍵如何導致死鎖(在Postgresql 9.2及更低版本中)。

讓我們說有一個子表引用到父表:

CREATE TABLE time_slot(
    id int primary key, 
    num_reservations int 
); 

CREATE TABLE reservation(
    time_slot_id int, 
    created_timestamp timestamp, 
    CONSTRAINT time_slot_fk FOREIGN KEY (time_slot_id) 
    REFERENCES time_slot(id) 
); 

INSERT INTO time_slot values(1, 0); 
INSERT INTO time_slot values(2, 0); 

假設在子表中的FK列在會話中的一個被修改時,將觸發普通插入語句(測試這種行爲,開在SQL殼牌(PSQL)一個會話,並關閉自動提交關閉,或使用begin語句啓動事務:

BEGIN; 
INSERT INTO reservation VALUES(2, now()); 

當子表中的FK列被修改,DBMS將查找父表,以保證父母的存在記錄。

如果插入的值在引用(父)表中不存在 - DBMS中斷事務並報告錯誤。

如果值存在,記錄被插入到子表中,但DBMS必須確保事務完整性 - 在事務結束之前,沒有其他事務可以刪除或修改父表中的引用記錄(直到插入到子表中承諾)。

PostgreSql 9.2(及以下版本)確保數據庫完整性,在這種情況下,在父表中的記錄上放置一個讀共享鎖。讀共享鎖不會阻止讀者從表中讀取鎖定的記錄,但可防止編寫者在共享模式下執行鎖定的記錄。

好的 - 現在我們在會話1所分發的子表中創建了一條新記錄(會話1在此記錄上放置了寫鎖),並將讀共享鎖放在父表中的記錄2上。該交易尚未提交。

假設一個會話2開始在同一個事務,引用相同的記錄在父表:

BEGIN; 
INSERT INTO reservation VALUES(2, now()); 

查詢執行罰款,沒有任何錯誤 - 它插入一個新的記錄到子表,並還會在父表中的記錄2上放置一個共享讀鎖。共享鎖不會發生衝突,許多事務可以在共享讀取模式下鎖定記錄,而不必等待其他事務(只有寫入鎖衝突)。

現在(幾毫秒後)的會話1火災(作爲同一事務的一部分)這樣的命令:

UPDATE time_slot 
SET num_reservations = num_reservations + 1 
WHERE id = 2; 

在Postgres的9.2以上命令「掛起」並正在等待共享鎖,放置通過會話2.

而現在,假設相同的命令,幾毫秒後,在進程中運行2:

UPDATE time_slot 
SET num_reservations = num_reservations + 1 
WHERE id = 2; 

這個命令應該是「掛」,並應等待寫鎖,放置由UPDA記錄在案會議1的TE。

但結果是:

BŁĄD: wykryto zakleszczenie 
SZCZEGÓŁY: Proces 5604 oczekuje na ExclusiveLock na krotka (0,2) relacji 41363 bazy danych 16393; zablokowany przez 381 
6. 
Proces 3816 oczekuje na ShareLock na transakcja 1036; zablokowany przez 5604. 
PODPOWIEDŹ: Przejrzyj dziennik serwera by znaleźć szczegóły zapytania. 

( 「zakleszczenie」 的意思是 「僵局」, 「BLAD」 的意思是 「錯誤」)

  • 更新命令從會話2正在嘗試在會話1鎖定的記錄2上放置寫鎖定
  • 會話1正嘗試在同一記錄上放置寫鎖定,由會話2鎖定(在共享模式下)
  • ----> ......死鎖。

    死鎖可以通過將一個寫鎖使用SELECT FOR UPDATE

    上述測試情況下,父表不會引起死鎖PostgreSQL中9.3(嘗試)來防止 - 在9.3他們改進在這樣的情況下鎖定行爲。



------------編輯 - 其他問題-------------------

why does the insert statement not release the lock after it is done? Or does it remain for the entire transaction which is why not using a transaction does not cause a deadlock?

修改事務中的數據(插入,更新,刪除)的所有語句都會鎖定已修改的記錄。這些鎖在事務結束之前保持活動 - 通過發出提交或回滾。

由於autocommit在JDBC連接斷開,連續SQL命令automaticaly分組到一個交易

的解釋是在這裏:
http://docs.oracle.com/javase/7/docs/api/java/sql/Connection.html#setAutoCommit%28boolean%29

If a connection is in auto-commit mode, then all its SQL statements will be executed and committed as individual transactions. Otherwise, its SQL statements are grouped into transactions that are terminated by a call to either the method commit or the method rollback.



How does the SELECT FOR UPDATE prevent the deadlock?


SELECT FOR UPDATE在記錄上放置寫鎖定。這是整個事務中的第一個命令,並且鎖定在開始處。當另一個事務開始時(在另一個會話中),也會執行SELECT FOR UPDATE,嘗試鎖定相同的記錄。寫鎖衝突 - 兩個事務不能同時鎖定相同的記錄 - 因此第二個事務的SELECT FOR UPDATE處於等待狀態,並且正在等待,直到第一個事務釋放鎖(通過發出提交或回滾) - 實際上第二個事務正在等待直到整個第一筆交易結束。

在第一種情況下,INSERT語句的地方有兩個鎖:
- 在預約表
插入的記錄寫鎖定 - 在由外鍵約束引用的TIME_SLOT表上記錄的讀取共享鎖

讀共享鎖不衝突 - 兩個或多個事務可以在共享模式下鎖定相同的記錄,並且可以繼續執行 - 不必等待對方。但後來,當UPDATE在同一個事務中發出時,試圖在已經鎖定在共享模式的相同記錄上放置一個寫鎖,這會導致死鎖。


Would placing the increment first also prevent the deadlock?


是的,你是對的。這可以防止死鎖,因爲在事務開始時在寫入記錄時會寫入鎖定。另一個事務也嘗試更新相同的記錄at the beginning,並且此時必須等待,因爲該記錄已被其他會話鎖定(處於寫入模式)。

+0

這很棒,正是我在找的!儘管如此,還是有一些問題:爲什麼插入語句在完成後不釋放鎖?還是它保留整個交易,這就是爲什麼不使用交易不會導致死鎖? SELECT FOR UPDATE如何防止死鎖?首先增加增量還可以防止死鎖? – Mike

+0

我已經添加到我的回答一些其他問題的意見。 – krokodilko

+0

感謝您的額外信息,我想我現在明白了。再次感謝您的時間和優雅的回答。 – Mike

0

雖然我還是不明白它,我補充說:

SELECT * FROM time_slot WHERE id = ? FOR UPDATE 

作爲交易中的第一個陳述。這似乎解決了我的問題,因爲我不再陷入僵局。

我仍然喜歡有人給出正確的答案,並向我解釋這一點。

+0

我猜在'reservation'表列上有一個引用'time_slot'表的外鍵約束,如果是,它會導致死鎖。 – krokodilko

+0

這是正確的。我沒有想到這一點。我錯誤地認爲插入到'reservation'表中並沒有涉及到這個問題,因爲它是一個不同的表,但我忘記了外鍵約束。任何人都可以告訴我外鍵約束如何導致這種僵局? – Mike