Sql-Server

觸發器上的鎖升級問題

  • May 23, 2013

我繼承了一個 SQL Server 2005 數據庫,該數據庫每天會發生 2-3 次死鎖。

我已將其跟踪到白天執行並插入帶有觸發器的表中的計劃作業。

觸發器由對另一個表的 10 次更新組成,標準略有不同。死鎖發生在觸發器中。

當一個人提出一個應用程序並且工作正在執行時,就會發生死鎖。應用程序插入到與計劃作業相同的表中。

在此處輸入圖像描述

查看跟踪似乎發生在程序 1 獲得鍵鎖,程序 2 獲得頁鎖,然後程序 1 將鍵鎖升級為頁鎖並且程序 2 嘗試獲得鍵鎖時。

我添加了缺失的索引,這似乎有所幫助,但它仍在發生。我不是 DBA,因此對於解決此問題的方法的任何建議將不勝感激。

我添加了一個指向死鎖 xml 的連結——這是我為複制問題所做的測試。

死鎖xml

最終解決方案需要包含所有索引的 tableA 的創建腳本和涉及的兩個 UPDATE 語句的執行計劃。話雖如此,我們仍然可以將提供的死鎖圖與 SQL Server 如何執行更新的知識結合起來,並且很有可能解決這個問題。

此死鎖涉及兩個 spid 對錶 A、58 和 59 執行更新。

Spid 58 的查詢:

Update b set  syd_id=d.syd_id    
       from tableA b    
       inner join tableC d with (nolock) on b.syd_pers_id=d.syd_pers_id    
       --and b.game='es_lotto'    
       and b.game=d.game  and isnull(b.syd_id,0)=0     

Spid 59 的查詢:

Update b set Draw_Date=d.draw_date    
       from tableA b
       inner join tableB d with (nolock) on b.draw_no=d.draw_no    
       and left(b.game,2)='UK' and b.Draw_Date is null     

在這個死鎖中,涉及到兩個資源:

  1. 索引 IX_tableA_Draw_Date 上的“鍵鎖”。
  • 由 spid 58 擁有,模式 = ‘X’(獨家)
  • 由 spid 59 請求,模式 = ‘U’(更新)
  1. tableA 上不同的未指定索引的“頁面”鎖
  • 由 spid 59 擁有,模式 = ‘IX’(意圖獨占)
  • 由 spid 58 要求,模式 = ‘U’(更新)

還使用了 3 種不同的鎖所有權模式(U、X、IX):

  1. “U”或更新鎖。從概念上講,UPDATE 語句的 sql 計劃有兩個部分:一個“選擇”部分,其中標識要修改的數據,以及一個“修改”部分,其中實際發生修改。在“選擇”部分使用更新鎖以保護行不被修改,直到我們開始實際修改它們(此時獲取 X 鎖)。更新鎖與“S”(共享)鎖兼容,因此讀者不會被阻塞,但是它們與 X、IX 和其他 U 鎖不兼容。
  2. “X”或獨占鎖。排他鎖用於保護在插入、更新和刪除期間被修改的數據。它們與所有其他鎖類型不兼容,並確保只有一個寫入者可以同時訪問資源。
  3. “IX” 或 Intent Exclusive Lock。在較低級別的資源獲得 X 鎖之前,對較高級別的資源進行意向排他鎖。例如,在對行使用 X 鎖之前,會在關聯頁和整個表上都使用 IX 鎖。IX 鎖表示 X 鎖存在於較低級別。作為鎖升級的一部分,它們也可以更改為 X 鎖。IX 鎖與 X 鎖不兼容,因此如果兩個事務在一個表上具有 IX 鎖,則兩個事務都不能將它們的頁鎖或行鎖升級到表級別。Intent 鎖(包括 IX)是一種優化,它允許 SQL 檢查一個更高級別的鎖,而不是檢查數千個較低級別的鎖。

現在,讓我們詳細檢查 spid 58 的 UPDATE 查詢。我們從死鎖圖中知道 spid 58 在 IX_tableA_Draw_Date 中的一行上有一個排他鎖。因此,我們知道 syd_id 要麼在索引鍵中,在聚集索引鍵中,要麼是包含列(我將忽略 spid 58 已經擁有觸發器中先前語句中的鎖的可能性)。

Spid 58 還試圖在不同索引中的頁面上獲取更新鎖。這說明了兩件事:

  1. 我們知道,在修改部分開始後,更新的選擇部分仍在讀取和鎖定數據。這意味著沒有阻塞運算符影響計劃中 tableA 中的行。例如,如果從 tableA 讀取行、排序並在 MERGE 連接中使用;然後在獲取任何排他鎖之前將讀取所有行(並進行 U 鎖定)。事實並非如此。
  2. 我們正在嘗試在 PAGE 而不是 ROW 上獲取更新鎖,因此很可能我們正在執行索引掃描。

我們還可以假設預計會更新相對少量的行(由於多列連接結合在插入觸發器中更新空值或 0,這感覺不像是更新大部分行的查詢)。考慮到所有這些,很可能 UPDATE 正在使用掃描我們的神秘索引(不知道它是集群的還是非集群的)從 tableA 讀取行,並對 tableC 執行 LOOP JOIN 以獲取 tableC.syd_id .

對 spid 59 執行類似的分析,很可能我們正在尋找“Draw_Date 為空”的 IX_tableA_Draw_Date,讀取(並在 ROWS 上獲取更新鎖),針對 tableB 進行 LOOP JOIN,然後獲取 X ROW 鎖並更新我們的“神秘”索引(頁面上的 IX 鎖意味著行上有 X 鎖)。


我們在哪?我們接下來要做什麼?

我們知道我們有兩個更新語句。兩者都在讀取和寫入同一對索引(交換了讀取和寫入)。在他們已經更新了一些行之後,兩者都在等待更新鎖。Spid 58 正在讀取頁面,59 正在讀取行。兩者都在寫入和鎖定行。鎖升級不是一個因素,因為行鎖 -> 表鎖(不是頁鎖)。

如果 Spid 58 在神秘索引上使用行鎖而不是頁鎖,那麼只有兩個查詢更新同一行時才會出現死鎖。您可以使用 ROWLOCK 查詢提示來實現這一點,但是您會面臨增加鎖升級到表鎖的風險,這可能會更糟。

另一種方法是將更新分成兩部分。

select id, --Not sure what the PK is...
   syd_id
into #tmp 
from tableA inner join tableB (nolock) on b.syd_pers_id=d.syd_pers_id    
   --and b.game='es_lotto'    
   and b.game=d.game  and isnull(b.syd_id,0)=0

UPDATE tableA SET syd_id = t.syd_id from tableA d inner join #tmp t on t.id = d.id 

這將改變鎖定的順序,因為非聚集索引只會鎖定寫入,而不是讀取。

推薦

死鎖是 RDBMS 的自然副產品。即使你做的每件事都“正確”,你也不能總是消除它們。在每天 3-4 次死鎖時,正確的方法可能是將語句包裝在一些錯誤處理中,以便在發生死鎖時重新嘗試。

http://msdn.microsoft.com/en-us/library/aa175791(v=sql.80).aspx有一個 SQL 2005 的範例。但是,由於這是在觸發器中,您可能需要添加錯誤處理在觸發觸發器的 INSERT 語句之外。

引用自:https://dba.stackexchange.com/questions/39747