SQL Server里因丟失索引造成的死鎖


在今天的文章里我想演示下SQL Server里在表上丟失索引如何引起死鎖(deadlock)的。為了准備測試場景,下列代碼會創建2個表,然后2個表都插入4條記錄。

 1 -- Create a table without any indexes
 2 CREATE TABLE Table1
 3 (
 4     Column1 INT,
 5     Column2 INT
 6 )
 7 GO
 8 
 9 -- Insert a few record
10 INSERT INTO Table1 VALUES (1, 1)
11 INSERT INTO Table1 VALUES (2, 2)
12 INSERT INTO Table1 VALUES (3, 3)
13 INSERT INTO Table1 VALUES (4, 4)
14 GO
15 
16 -- Create a table without any indexes
17 CREATE TABLE Table2
18 (
19     Column1 INT,
20     Column2 INT
21 )
22 GO
23 
24 -- Insert a few record
25 INSERT INTO Table2 VALUES (1, 1)
26 INSERT INTO Table2 VALUES (2, 2)
27 INSERT INTO Table2 VALUES (3, 3)
28 INSERT INTO Table2 VALUES (4, 4)
29 GO

在我向你重現死鎖前,先看下列的代碼,它是個簡單的UPDATE語句,在第1個表里更新一個指定行。

1 -- Acquires an Exclusive Lock on the row
2 UPDATE Table1 SET Column1 = 3 WHERE Column2 = 1

因為在Column2上沒有索引定義,對於我們的UPDATE語句,查詢優化器在執行計划里必須選擇表掃描(Table Scan)運算符來查找符合的記錄:

 

這就是說我們必須掃描整個堆表來找我們想更新的行。在那個情況下,SQL Server用排它鎖(Exclusive Lock)鎖定表里的第1行。當你在不同的會話執行一個SELECT語句,引用另一個堆表里“將發生”的行,表掃描(Table Scan)運算符會阻塞,因為首先你必須讀取所有堆表里“已發生”的行,即獲取你查詢里邏輯請求的行。

-- This query now requests a Shared Lock, but get's blocked, because the other session/transaction has an Exclusive Lock on one row, that is currently updated
SELECT Column1 FROM Table1
WHERE Column2 = 4

表掃描(Table Scan)默認意味這你必須掃描整個表,因此你必須在每條記錄上獲得共享鎖(Shared Lock)——即使在你邏輯上不請求的記錄上。如果你用不同的順序,在不同的會話里訪問2個表,當你從同個表嘗試讀寫時,這個情況會導致死鎖情形。下面代碼顯示來自第1個查詢的事務: 

 1 BEGIN TRANSACTION
 2 
 3 -- Acquires an Exclusive Lock on the row
 4 UPDATE Table1 SET Column1 = 3 WHERE Column2 = 1
 5 
 6 -- Execute the query from Session 2...
 7 -- This query acquires an Exclusive Lock on one row from Table2...
 8 
 9 -- This query now requests a Shared Lock, but get's blocked, because the other session/transaction has an Exclusive Lock on one row, that is currently updated
10 SELECT Column1 FROM Table2
11 WHERE Column2 = 3
12 
13 ROLLBACK TRANSACTION
14 GO

下面顯示來自第2個事務的代碼:

 1 BEGIN TRANSACTION
 2 
 3 -- Acquires an Exclusive Lock on the row
 4 UPDATE Table2 SET Column1 = 5 WHERE Column2 = 2
 5 
 6 -- Continue with the query from Session 2...
 7 -- This query now requests a Shared Lock, but get's blocked, because the other session/transaction has an Exclusive Lock on one row, that is currently updated
 8 
 9 -- This query now requests a Shared Lock, but get's blocked, because the other session/transaction has an Exclusive Lock on one row, that is currently updated
10 SELECT Column1 FROM Table1
11 WHERE Column2 = 4
12 
13 ROLLBACK TRANSACTION
14 GO

從2個事務可以看到,2個表在不同的順序里被訪問。如果時機合適,在同個時間運行這2個事務會導致死鎖(deadlock)情形。假設下列的執行順序:

  1. 在Table1上第1個事務運行UPDATE語句。
  2. 在Table2上第2個事務運行UPDATE語句。
  3. 在Table2上第1個事務運行SELECT語句。這個SELECT語句會阻塞,因為表掃描(Table Scan)運算符想要在行上獲得的共享鎖(Shared Lock),已經被第2個事務排它鎖(exclusively lock)鎖定。
  4. 在Table1上第2個事務運行SELECT語句。這個SELECT語句會阻塞,因為表掃描(Table Scan)運算符想要在行上獲得的共享鎖(Shared Lock),已經被第1個事務排它鎖(exclusively lock)鎖定。

下圖演示了這個死鎖情形:

 

現在2個事務相互阻塞,因此在SQL Server里你引起了死鎖。在那個情況下死鎖監控器(Deadlock Monitor)后台進程踢入,進行最“便宜”的事務的回滾(基於事務需要寫入事務日志的字節數)。

你可以在2個表里通過為Column2提供一個索引來輕松解決這個死鎖。在那個情況下SQL Server可以進行符合列的查找(Seek)運算符操作,因此當你執行SELECT語句時,可以跳過已經在索引葉子層的鎖定行:

1 CREATE NONCLUSTERED INDEX idx_Column2 ON Table1(Column2)
2 CREATE NONCLUSTERED INDEX idx_Column2 ON Table2(Column2)
3 GO

下圖演示了現在的死鎖情形是怎樣的:

使用查找操作你可以跳過索引葉子層的鎖定行,你可以避免我們已經討論過的死鎖。因此當你在你的數據庫看到死鎖情形時,仔細看下你的索引戰略(設計),這非常重要!在SQL Server里,索引一直是一個很重要的東西——始終記住這個!

感謝關注! 

參考文章:

https://www.sqlpassion.at/archive/2014/11/24/deadlocks-caused-by-missing-indexes-in-sql-server/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM