死鎖經常與正常阻塞混淆。當一個事務請求對另一個事務鎖定的資源加鎖時,請求加鎖的事務會等待直到鎖被釋放。預設情況下,除非設定了 LOCK_TIMEOUT,否則 SQL Server 事務不會超時。請求事務被阻塞,而不是死鎖,因為請求事務沒有做任何事情來阻塞持有鎖的事務。最終,持有鎖的事務將完成並釋放鎖,然後請求事務將獲得鎖並繼續。死鎖幾乎可以立即解決,而阻塞理論上可以無限期地持續下去。
死鎖可能發生在任何有多個執行緒的系統中,而不僅僅是關聯式資料庫管理系統,而且可能發生在資料庫物件鎖以外的資源上。例如,多執行緒作業系統中的執行緒可能會獲取一個或多個資源,如記憶體塊。如果正在獲取的資源當前為另一個執行緒所有,則第一個執行緒可能需要等待擁有資源的執行緒釋放目標資源。等待的執行緒對擁有該特定資源的執行緒具有依賴性。在 SQL Server 資料庫引擎例項中,會話在獲取記憶體或執行緒等非資料庫資源時可能會出現死鎖。
當對錶進行分割槽並將 alter table 的 lock_escalation 設定為 auto 時,也可能出現死鎖。當 lock_escalation 設定為 auto 時,由於允許 sql server 資料庫引擎在 hobt 級別而不是在表級別鎖定表分割槽,因此併發性會提高。但是,當不同事務在表中持有分割槽鎖,並希望在其他事務分割槽的某個地方獲得鎖時,就會導致死鎖。將 lock_escalation 設定為 table 可以避免這種死鎖;不過,這種設定會降低併發性,因為它會迫使分割槽的大型更新等待表鎖。
可能造成死鎖的資源
每個使用者會話都可能有一個或多個任務在執行,每個任務都可能獲取或等待獲取資源。以下型別的資源可能會導致阻塞,從而造成死鎖。
1.鎖
等待獲取物件、頁、行、後設資料和應用程式等資源的鎖可能會導致死鎖。
2.工作執行緒
等待可用工作執行緒的佇列任務可能會導致死鎖。如果佇列任務擁有的資源阻塞了所有工作執行緒,就會導致死鎖。例如,會話 S1 啟動事務並獲得行 r1 上的共享 (S) 鎖,然後進入休眠狀態。在所有可用工作執行緒上執行的活動會話正試圖獲取 r1 行上的獨佔 (X) 鎖。由於會話 S1 無法獲取工作執行緒,因此它無法提交事務並釋放 r1 行上的鎖。這就導致了死鎖。
3.記憶體
當併發請求等待的記憶體授予無法用可用記憶體滿足時,就會出現死鎖。例如,兩個併發查詢 Q1 和 Q2 作為使用者定義函式執行,分別獲取 10 MB 和 20 MB 記憶體。如果每個查詢需要 30 MB,而可用記憶體總量為 20 MB,那麼 Q1 和 Q2 必須等待對方釋放記憶體,從而導致死鎖。
4.並行查詢執行相關資源。
與交換埠相關聯的協調者、生產者或消費者執行緒可能會相互阻塞,導致死鎖,通常是在包括至少一個不屬於並行查詢的其他程序時。此外,當並行查詢開始執行時,SQL Server 會根據當前的工作量確定並行程度或工作執行緒的數量。如果系統工作量發生意外變化,例如伺服器上開始執行新查詢或系統工作執行緒用完,就可能出現死鎖。
5.多活動結果集 (MARS) 資源
這些資源用於控制 MARS 下多個活動請求的交錯。
·使用者資源。
當執行緒在等待可能由使用者應用程式控制的資源時,該資源會被視為外部或使用者資源,並被視為鎖。
·會話mutex。
在一個會話中執行的任務是交錯的,這意味著在給定時間內,該會話下只能執行一個任務。在任務執行之前,它必須擁有對會話互斥器的獨佔訪問許可權。
·事務mutex。
在一個事務中執行的所有任務都是交錯的,這意味著在給定時間內,只有一個任務能在事務下執行。在任務執行之前,它必須擁有對事務互斥器的獨佔訪問許可權。
任務要在 MARS 下執行,必須獲得會話mutex。如果任務在事務中執行,則必須獲取事務mutex。這就保證了在給定會話和給定事務中,同一時間只有一個任務處於活動狀態。獲得所需的mutex後,任務就可以執行了。當任務完成或在請求中途放棄時,它將首先釋放事 mutex,然後按獲取的相反順序釋放會話mutex。然而,這些資源可能會出現死鎖。在下面的虛擬碼中,使用者請求 U1 和使用者請求 U2 這兩個任務執行在同一個會話中。
U1: Rs1=Command1.Execute("insert sometable EXEC usp_someproc"); U2: Rs2=Command2.Execute("select colA from sometable");
使用者請求 U1 執行的儲存過程已獲得會話mutex。如果儲存過程需要很長時間才能執行,SQL Server 資料庫引擎會認為儲存過程正在等待使用者的輸入。使用者請求 U2 在等待會話mutex時,使用者正在等待 U2 的結果集,而 U1 正在等待使用者資源。這種死鎖狀態在邏輯上可表示為
死鎖檢測
死鎖檢測由鎖監控執行緒執行,該執行緒會定期啟動對 SQL Server 資料庫引擎例項中所有任務的搜尋。以下描述了搜尋過程:
·預設時間間隔為 5 秒。
·如果鎖監控執行緒發現死鎖,死鎖檢測時間間隔會從 5 秒縮短到 100 毫秒,具體取決於死鎖發生的頻率。
·如果鎖監控執行緒不再發現死鎖,SQL Server 資料庫引擎會將搜尋間隔延長至 5 秒。
·如果檢測到死鎖,則假定下一個必須等待鎖的執行緒正在進入死鎖週期。檢測到死鎖後的頭幾次鎖等待將立即觸發死鎖搜尋,而不是等待下一次死鎖檢測間隔。例如,如果當前時間間隔為 5 秒,檢測到死鎖,那麼下一次鎖等待將立即啟動死鎖檢測。如果這個鎖等待是死鎖的一部分,它將立即被檢測到,而不是在下一次死鎖搜尋時被檢測到。
SQL Server 資料庫引擎通常只執行定期死鎖檢測。由於系統中遇到的死鎖數量通常很少,因此定期死鎖檢測有助於減少系統中死鎖檢測的開銷。
當鎖監控器對某個特定執行緒啟動死鎖搜尋時,它會識別執行緒正在等待的資源。然後,鎖監控器會找到該特定資源的所有者,並遞迴地繼續對這些執行緒進行死鎖搜尋,直到找到迴圈為止。以這種方式確定的迴圈會形成死鎖。
檢測到死鎖後,SQL Server 資料庫引擎會選擇其中一個執行緒作為死鎖受害者,從而結束死鎖。SQL Server 資料庫引擎會終止該執行緒正在執行的當前批處理,回滾死鎖受害者的事務,並嚮應用程式返回 1205 錯誤。回滾死鎖受害者的事務會釋放事務持有的所有鎖。這樣,其他執行緒的事務就可以解除鎖定並繼續執行。1205 死鎖受害者錯誤會在錯誤日誌中記錄涉及死鎖的執行緒和資源資訊。
預設情況下,SQL Server 資料庫引擎會選擇執行回滾成本最低的事務的會話作為死鎖受害者。另外,使用者也可以使用 SET DEADLOCK_PRIORITY 語句指定死鎖情況下會話的優先順序。DEADLOCK_PRIORITY 可以設定為 LOW、NORMAL 或 HIGH,也可以設定為範圍(-10 至 10)內的任意整數值。死鎖優先順序預設為 NORMAL。如果兩個會話的死鎖優先順序不同,則選擇優先順序較低的會話作為死鎖受害者。如果兩個會話的死鎖優先順序相同,則選擇回滾成本最低的事務會話。如果參與死鎖迴圈的會話具有相同的死鎖優先順序和相同的成本,則隨機選擇一個受害者。
在使用 CLR 時,死鎖監視器會自動檢測 managed procedures 中訪問的同步資源(死鎖器、讀寫鎖和執行緒連線)的死鎖。不過,死鎖是透過在被選中為死鎖受害者的儲存過程中丟擲異常來解決的。需要注意的是,異常不會自動釋放受害者當前擁有的資源;必須明確釋放這些資源。與異常行為一致,用於識別死鎖受害者的異常可以被捕獲並解除。
死鎖資訊工具
要檢視死鎖資訊,SQL Server 資料庫引擎以 system_health xEvent 會話、兩個trace標誌和 SQL Profiler 中的死鎖圖事件的形式提供監控工具。
官方推薦使用死鎖擴充套件事件。
SQL Profiler 會建立trace,但trace已於 2016 年過時,取而代之的是擴充套件事件。與trace相比,擴充套件事件的效能開銷要小得多,可配置性也高得多。可以考慮使用擴充套件事件死鎖事件來代替trace。
死鎖擴充套件事件
從 SQL Server 2012 (11.x) 開始,在 SQL Trace 或 SQL Profiler 中應使用 xml_deadlock_report 擴充套件事件 (xEvent) 代替死鎖圖事件類。
同樣從 SQL Server 2012 (11.x) 開始,當發生死鎖時,system_health 會話已捕獲包含死鎖圖的所有 xml_deadlock_report xEvent。由於預設啟用了 system_health 會話,因此無需配置單獨的 xEvent 會話來捕獲死鎖資訊。使用 xml_deadlock_report xEvent 捕捉死鎖資訊不需要額外的操作。
捕獲的死鎖圖通常有三個不同的節點:
·victim-list:死鎖受害者程序識別符號。
·程序列表:死鎖涉及的所有程序的資訊。
·資源列表:死鎖涉及的資源資訊。
開啟 system_health 會話檔案或環形緩衝區(ring buffer),如果記錄了 xml_deadlock_report xEvent,Management Studio 就會以圖形方式顯示死鎖涉及的任務和資源,如下例所示:
以下查詢可檢視 system_health 會話環形緩衝區捕獲的所有死鎖事件:
SELECT xdr.value('@timestamp', 'datetime') AS [Date], xdr.query('.') AS [Event_Data] FROM (SELECT CAST([target_data] AS XML) AS Target_Data FROM sys.dm_xe_session_targets AS xt INNER JOIN sys.dm_xe_sessions AS xs ON xs.address = xt.event_session_address WHERE xs.name = N'system_health' AND xt.target_name = N'ring_buffer' ) AS XML_Data CROSS APPLY Target_Data.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData(xdr) ORDER BY [Date] DESC;
以下是輸出結果集: