SQL Server-聚焦深刻理解死鎖以及避免死鎖建議(三十三)

前言

終於進入死鎖系列,前面也提到過我一直對隔離級別和死鎖以及如何避免死鎖等問題模棱兩可,因此才鼓起了從新學習SQL Server系列的勇氣,本節咱們來說講SQL Server中的死鎖,看到許多文章都只簡述不能這樣作,這樣作會致使死鎖,可是未理解其基本原理,下次遇到相似狀況依然會犯錯,因此基於瞭解死鎖原理而且獲得治療死鎖良方,博主不惜花費多天時間來學習死鎖最終總結出本文,如有敘述不當之處請在評論中指出。node

死鎖定義

死鎖是兩個或多個進程互相阻塞的狀況。兩個進程死鎖的例子是,進程A阻塞進程B且進程B阻塞進程B。涉及多個進程死鎖的例子是,進程A阻塞進程B,進程B阻塞進程C且進程C阻塞進程A。在任何一種狀況下,SQL Server檢測到死鎖,都會經過終止其中的一個事務盡心干預。若是SQL Server不干預,涉及的進程永遠陷於死鎖狀態。面試

除外另外指定,SQL Server選擇終止工做最少的事務,由於它便於回滾該事務的工做。可是,SQL Server容許用戶設置一個叫作DEADLOCK_PRIORITY的會話選項,能夠是範圍在-10~10之間的21個值中的任意值,死鎖優先級最低的進程將被做爲犧牲對象,而無論其作了多少工做。咱們能夠舉一個生活中常見和死鎖相似的例子,當在車道上行駛時,快到十字路口的紅燈時,此時全部的小車都已經就緒等待紅燈,當變綠燈時,此時有駕駛員發現走錯了車道,因而開始變換車道,可是別的車道都擁堵在一塊根本插不進去,駕駛員只有等待有空隙時再插進去,同時駕駛員車道上後面的小車又在等待駕駛員開到別的車道。這種狀況雖然不恰當,可是在必定程度上很好的表現了死鎖的狀況,因此在開車時儘可能別吵吵,不然誰都走不了,keep silence。sql

下面咱們來演示常見的一種死鎖狀況,而後咱們再來討論如何減小系統中死鎖的發生。數據庫

讀寫死鎖

在SQL Server數據庫中咱們打開兩個鏈接並確保都已鏈接到數據庫,在會話一中咱們試圖去更新Production.Products表中產品2的行。session

SET TRAN ISOLATION LEVEL READ COMMITTED 

BEGIN TRAN;

UPDATE Production.Products
SET unitprice += 1.00
WHERE productid = 2;

在會話2中再來打開一個事務,更新Sales.OrderDetails表中產品2的行,並使事務保持打開狀態併發

SET TRAN ISOLATION LEVEL READ COMMITTED
 
BEGIN TRAN

UPDATE Sales.OrderDetails
SET unitprice += 1.00
WHERE productid = 2;

此時上述會話一和會話二都用其會話的排他鎖且都能更新成功,下面咱們再來在會話一中進行查詢Sale.OrderDetails表中產品2的行並提交事務。app

SET TRAN ISOLATION LEVEL READ COMMITTED
 
BEGIN TRAN;

SELECT orderid, productid, unitprice
FROM Sales.OrderDetails
WHERE productid = 2;

COMMIT TRAN;

由於須要查詢Sales.OrderDetails表中產品2的行,可是在以前咱們更新產品2的行同時並未提交事務,由於查詢的共享鎖和排它鎖不兼容,最終致使查詢會阻塞,接下來咱們在會話二中再來查詢Producution.Products表中產品爲2的行。學習

SET TRAN ISOLATION LEVEL READ COMMITTED

BEGIN TRAN

SELECT productid, unitprice
FROM Production.Products
WHERE productid = 2;

COMMIT TRAN;

此時咱們看到在會話二中能成功查詢到Production.Products表中產品2的行,同時咱們再來看看會話一中查詢狀況。測試

上述死鎖算是最多見的死鎖狀況,在會話一(A進程)中去更新Production.Products表中產品2的行,在會話二(B進程)去更新Sales.OrderDetails表中產品2的行,可是接下來在會話一中去查詢Sales.OrderDetails表中產品2的行,此時B進程要等待A進程中未提交的事務進行提交,因此致使A進程將阻塞B進程,接着在會話二中去查詢Production.Products表中產品2的行,此時A進程要等待B進程中未提交的事務進行提交,因此致使B進程阻塞A進程,最終結果將是死鎖。因此到了這裏咱們可以很清楚地知道在兩個或多個事務中注意事務之間不能交叉進行。要是面試時忘記了腫麼辦,告訴你一個簡單的方法,當軍訓或者上體育課正步走時只有一、二、1,沒有一、二、2就行。優化

寫寫死鎖

想必你們大部分只知道上述狀況的死鎖,上述狀況是什麼狀況,咱們抽象一下則是不一樣表之間致使的死鎖,下面咱們來看看同一表中如何產生死鎖,這種狀況你們更加須要注意了。咱們首先建立死鎖測試表並對錶中列Id,Name建立惟一彙集索引,以下:

USE tempdb
GO
CREATE TABLE DeadlocksExample
(Id INT, Name CHAR(20), Company CHAR(50));
GO
CREATE UNIQUE CLUSTERED INDEX deadlock_idx ON DeadlocksExample (Id, Name)
GO

接下來在會話一中插入一條測試數據開啓事務可是並未提交,以下:

BEGIN TRAN
INSERT INTO dbo.DeadlocksExample
VALUES (1, 'Jeffcky', 'KS')

接下來再來打開一個會話二插入一條數據開啓事務可是並未提交,以下:

BEGIN TRAN
INSERT INTO DeadlocksExample
VALUES (10, 'KS', 'Jeffcky')

再來在會話一中插入一條數據。

INSERT INTO DeadlocksExample
VALUES (10, 'KS', 'Jeffcky')

此時這次插入將會阻塞,以下:

最後再來在會話二中插入一條數據

INSERT INTO DeadlocksExample
VALUES (1, 'Jeffcky', 'KS')

此時這次插入能進行可是會顯示死鎖信息,以下:

想必大多數狀況下看到的是經過不一樣表更新行產生的死鎖,在這裏咱們演示了在相同表經過插入行也會致使死鎖,死鎖真是無處不在。上述發生死鎖的主要緣由在於第二次在會話一中去插入相同數據行時此時因爲咱們建立了Id和Name的惟一彙集索引因此SQL Server內部會嘗試去讀取行致使插入阻塞,在會話一中去插入行同理,最終形成彼此等待而死鎖。爲了更深刻死鎖知識,咱們來看看如何從底層來探測死鎖,上述發生死鎖後,咱們經過運行以下語句來查詢死鎖圖:

SELECT XEvent.query('(event/data/value/deadlock)[1]') AS DeadlockGraph 
FROM ( SELECT XEvent.query('.') AS XEvent 
       FROM ( SELECT CAST(target_data AS XML) AS TargetData 
              FROM sys.dm_xe_session_targets st 
                   JOIN sys.dm_xe_sessions s 
                   ON s.address = st.event_session_address 
              WHERE s.name = 'system_health' 
                    AND st.target_name = 'ring_buffer' 
              ) AS Data 
              CROSS APPLY 
                 TargetData.nodes 
                    ('RingBufferTarget/event[@name="xml_deadlock_report"]')
              AS XEventData ( XEvent ) 
      ) AS src;

此時你將發現會出現以下xml的數據:

咱們點看死鎖圖來分析分析:

<deadlock>
  <victim-list>
    <victimProcess id="process17602d868" />
  </victim-list>
  <process-list>
    <process id="process17602d868" taskpriority="0" logused="300" waitresource="KEY: 2:2089670228247904256 (4e0d37de3c51)" waittime="4222" ownerId="49122" transactionname="user_transaction" lasttranstarted="2017-03-04T21:56:15.447" XDES="0x16db8c3a8" lockMode="X" schedulerid="4" kpid="8296" status="suspended" spid="59" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2017-03-04T21:56:47.080" lastbatchcompleted="2017-03-04T21:56:15.450" lastattention="1900-01-01T00:00:00.450" clientapp="Microsoft SQL Server Management Studio - 查詢" hostname="WANGPENG" hostpid="1640" loginname="wangpeng\JeffckyWang" isolationlevel="read committed (2)" xactid="49122" currentdb="2" lockTimeout="4294967295" clientoption1="671090784" clientoption2="390200">
      <executionStack>
        <frame procname="adhoc" line="1" stmtstart="84" sqlhandle="0x02000000ea13d9115e8a4d429bc3d549e9053a3a784358020000000000000000000000000000000000000000">
INSERT INTO [DeadlocksExample] values(@1,@2,@3)    </frame>
        <frame procname="adhoc" line="1" sqlhandle="0x020000009882c20809f279b6638fea1ef34b7986efb6b60a0000000000000000000000000000000000000000">
INSERT INTO DeadlocksExample
VALUES (1, 'Jeffcky', 'KS')    </frame>
      </executionStack>
      <inputbuf>
INSERT INTO DeadlocksExample
VALUES (1, 'Jeffcky', 'KS')   </inputbuf>
    </process>
    <process id="process17602dc38" taskpriority="0" logused="300" waitresource="KEY: 2:2089670228247904256 (381c351990d5)" waittime="20467" ownerId="49022" transactionname="user_transaction" lasttranstarted="2017-03-04T21:56:06.070" XDES="0x16db8d6a8" lockMode="X" schedulerid="4" kpid="2684" status="suspended" spid="54" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2017-03-04T21:56:30.837" lastbatchcompleted="2017-03-04T21:56:06.070" lastattention="1900-01-01T00:00:00.070" clientapp="Microsoft SQL Server Management Studio - 查詢" hostname="WANGPENG" hostpid="1640" loginname="wangpeng\JeffckyWang" isolationlevel="read committed (2)" xactid="49022" currentdb="2" lockTimeout="4294967295" clientoption1="671090784" clientoption2="390200">
      <executionStack>
        <frame procname="adhoc" line="1" stmtstart="84" sqlhandle="0x02000000ea13d9115e8a4d429bc3d549e9053a3a784358020000000000000000000000000000000000000000">
INSERT INTO [DeadlocksExample] values(@1,@2,@3)    </frame>
        <frame procname="adhoc" line="1" sqlhandle="0x02000000579d610429b0df7caee58044a9e5b493ea0d8e450000000000000000000000000000000000000000">
INSERT INTO DeadlocksExample
VALUES (10, 'KS', 'Jeffcky')    </frame>
      </executionStack>
      <inputbuf>
INSERT INTO DeadlocksExample
VALUES (10, 'KS', 'Jeffcky')   </inputbuf>
    </process>
  </process-list>
  <resource-list>
    <keylock hobtid="2089670228247904256" dbid="2" objectname="tempdb.dbo.DeadlocksExample" indexname="1" id="lock172b46e80" mode="X" associatedObjectId="2089670228247904256">
      <owner-list>
        <owner id="process17602dc38" mode="X" />
      </owner-list>
      <waiter-list>
        <waiter id="process17602d868" mode="X" requestType="wait" />
      </waiter-list>
    </keylock>
    <keylock hobtid="2089670228247904256" dbid="2" objectname="tempdb.dbo.DeadlocksExample" indexname="1" id="lock172b48b00" mode="X" associatedObjectId="2089670228247904256">
      <owner-list>
        <owner id="process17602d868" mode="X" />
      </owner-list>
      <waiter-list>
        <waiter id="process17602dc38" mode="X" requestType="wait" />
      </waiter-list>
    </keylock>
  </resource-list>
</deadlock>

東西貌似比較多哈,彆着急我也是菜鳥,咱們慢慢看,咱們將其摺疊,重點是分爲以下兩塊:

死鎖最重要的兩個節點則是如上process和resource,咱們再來一塊一塊分析,首先看process-list

 如上咱們可以很清晰的看到關於死鎖的全部細節,咱們查詢的SQL語句、隔離級別以及事務開始和結束的時間等更多詳細介紹,咱們再來看看resource-list

而resource-list則列舉出了關於鎖的全部資源,如上列舉出了每一個進程獲取到的鎖以及請求的鎖。咱們從上看出經過 objectname 來標識數據庫關於死鎖的表,咱們能夠經過 associatedObjectId 關聯對象Id來獲得代表和索引,運行以下查詢:

SELECT OBJECT_NAME(p.object_id) AS TableName , 
       i.name AS IndexName 
FROM sys.partitions AS p 
     INNER JOIN sys.indexes AS i ON p.object_id = i.object_id 
                                    AND p.index_id = i.index_id 
WHERE partition_id = 2089670228247904256

上述resource-list節點下有兩個重要的子節點:owner-list和waiter-list,owner-list從字面意思理解則是擁有鎖的進程,同理waiter-list則是請求鎖而且等待這個擁有鎖的進程釋放的進程。咱們可以看到上述涉及到的都是排它鎖。resource-list中的過程大概以下:

(1)進程dc38獲取在表 DeadlocksExample 上鍵中的排它鎖。

(2)進程d868獲取在表 DeadlocksExample 上鍵中的排它鎖。

(3)進程d868請求在表 DeadlocksExample 上鍵中的排它鎖。

(4)進程dc38請求在表 DeadlocksExample 上鍵中的排它鎖。

因此爲什麼通常不推薦使用聯合主鍵,若使用聯合主鍵則該狀況如上述所述,此時兩個列默認建立則是惟一彙集索引,當有併發狀況產生時會就有可能致使在同一表中插入相同的值此時將致使死鎖狀況發生,想必大部分使用聯合主鍵的情景應該是在關聯表中,將兩個Id標識爲聯合主鍵,此時咱們應該從新設置一個主鍵不管是INT或者GUID也好都比聯合主鍵要強不少。

實戰拓展 

上述咱們大概瞭解了下死鎖圖以及相關節點解釋,接下來咱們來演示幾種常見的死鎖並逐步分析。咱們來看看。

避免邏輯死鎖

咱們建立以下測試表並默認插入數據:

CREATE TABLE Table1
(
    Column1 INT,
    Column2 INT
)
GO
INSERT INTO Table1 VALUES (1, 1), (2, 2),(3, 3),(4, 4)
GO


CREATE TABLE Table2
(
    Column1 INT,
    Column2 INT
)
GO
INSERT INTO Table2 VALUES (1, 1), (2, 2),(3, 3),(4, 4)
GO

此時咱們進行數據更新對Column2,以下:

UPDATE Table1 SET Column1 = 3 WHERE Column2 = 1

此時因爲咱們對列Column2沒有建立索引,因此會形成SQL Server引擎會進行全表掃描去堆棧中找到咱們須要更新的數據,同時呢SQL Server會對該行更新的數據獲取一個排它鎖,當咱們進行以下查詢時

SELECT Column1 FROM Table1
WHERE Column2 = 4

此時將獲取共享鎖,即便上述更新和此查詢語句在不一樣的會話中都不會形成阻塞,雖然排它鎖和共享鎖不兼容,由於上述更新數據的內部事務已經提交,因此排它鎖已經釋放。下面咱們來看看死鎖狀況,咱們打開兩個會話並確保會話處於鏈接狀態。

在會話一中咱們對錶一上的Column1列進行更新經過篩選條件Column2同時開啓事務並未提交,以下:

BEGIN TRANSACTION
 
UPDATE Table1 SET Column1 = 3 WHERE Column2 = 1

會話二同理

BEGIN TRANSACTION
 
UPDATE Table2 SET Column1 = 5 WHERE Column2 = 2

同時去更新兩個會話中的數據。接下來再在會話一中更新表二中的數據行,以下:

BEGIN TRANSACTION
 
SELECT Column1 FROM Table2
WHERE Column2 = 3
 
ROLLBACK TRANSACTION

在讀寫死鎖中咱們已經演示此時查詢會形成堵塞,就再也不貼圖片了。

同理在會話一中更新表一中的數據行。

BEGIN TRANSACTION
 
SELECT Column1 FROM Table1
WHERE Column2 = 4
 
ROLLBACK TRANSACTION
GO

此時運行會話二中的語句,將獲得以下死鎖信息,固然兩者必然有一個死鎖犧牲品,至因而哪一個會話,那就看SQL Server內部處理機制。

上述關於表一和表二死鎖的狀況,大概以下圖所示

 

上述因爲沒有創建索引致使全表掃描因此對於每行記錄都會獲取一個共享鎖,可是呢,更新數據進程又爲提交事務最終會致使死鎖,其實咱們能夠經過創建索引來找到匹配的行同時會繞過在葉子節點上被鎖定的行,那麼應該建立什麼索引呢,對篩選條件建立過濾索引?顯然是不行的,由於查詢出的列仍是到基表中去全表掃描,因此咱們常見覆蓋索引,以下:

CREATE NONCLUSTERED INDEX idx_Column2 ON Table1(Column2) INCLUDE(column1)
CREATE NONCLUSTERED INDEX idx_Column2 ON Table2(Column2) INCLUDE(column1)

當咱們再次從新進行如上動做時,你會發如今會話一中進行查詢時此時將不會致使阻塞,直接能查詢出數據,以下:

因此經過上述表述咱們知道好的索引設計可以減小邏輯衝突上的死鎖。 

避免範圍掃描和SERIALIZABLE死鎖

好比咱們要進行以下查詢。

SELECT CustomerIDFROM Customers WHERE CustomerName = @p1

一旦有併發則有可能形成幻影讀取即剛開始數據爲空行,可是第二次讀取時則存在數據,因此此時咱們設置更高的隔離級別,以下:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

經過設置該隔離級別即便剛開始數據爲空行,它能保證再次讀取時返回的數據必定爲空行,經過鎖住 WHERE CustomerName = @p1 而且它會鎖住值等於@p1的全部記錄,咱們常常有這樣的需求,當數據存在時則更新,不存在時則插入,若是你沒有想到併發狀況的發生,估計到時投訴將落在你身上,因此爲了解決兩次讀取一致的狀況咱們設置最高隔離級別,以下:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
 
BEGIN TRANSACTION
IF EXISTS ( SELECT  1
            FROM    [dbo].[Customers] WITH ( ROWLOCK )
            WHERE   CustomerName = @p1 )
    UPDATE  dbo.Customers
    SET     LatestOrderStatus = NULL ,
            OrderLimit = 0
    WHERE   CustomerName = @p1;
ELSE
    INSERT  INTO dbo.Customers
            ( CustomerName ,
              RegionID ,
              OrderLimit ,
              LatestOrderStatus
            )
    VALUES  ( @p1 ,
              0 ,
              0 ,
              NULL
            );
COMMIT TRANSACTION

上述假設咱們對CustomerName創建了惟一索引並加了行鎖來鎖住單行數據,看起來so good,實際上有沒有問題呢。若是當CustomerName = 'Jeffcky'在該行上面的CustomerName = 'Jeffcky1',在其下方的CustomerName = 'Jeffcky2',此時經過設置最高隔離級別將鎖住這三行來阻止任何數據的插入,咱們能夠將其叫作範圍共享鎖,若是在不一樣會話中在該範圍內插入不一樣行,此時極有可能形成死鎖,你覺得設置最高隔離級別就萬事大吉了嗎。那麼該如何解決這個麻煩呢,咱們能夠經過Merge來避免該死鎖,由於Merge操做爲單原子操做,咱們不在須要最高隔離級別,可是貌似有潛在的bug發生未驗證過,同時該Merge我也未用過。

這個問題是在博問中看到dudu老大提出插入重複數據而想到(博問地址:https://q.cnblogs.com/q/90745/),dudu老大所給語句爲以下SQL語句:

IF NOT EXISTS(SELECT 1 FROM [Relations] WHERE [UserId]=@UserId AND [RelativeUserId]=@RelativeUserId AND IsActive=1)
BEGIN
    BEGIN TRANSACTION
    INSERT INTO [Relations]([UserId], [RelativeUserId]) 
    VALUES (@UserId,@RelativeUserId)
    UPDATE [Users] SET FollowingCount=FollowingCount+1 WHERE UserID=@UserId
    UPDATE [Users] SET FollowerCount=FollowerCount+1 WHERE UserID=@RelativeUserId
    COMMIT TRANSACTION
END

當時我所給出的答案爲以下:

INERT INTO.....SELECT ...FROM WHERE NOT EXSITS(SELECT 1 FROM...)

對應上述狀況咱們將上述隔離級別去掉利用兩個語句來操做,以下:

UPDATE  dbo.Customers
SET     LatestOrderStatus = NULL ,
        OrderLimit = 0
WHERE   CustomerName = @p1;
 
INSERT  INTO dbo.Customers
        ( CustomerName ,
          RegionID ,
          OrderLimit ,
          LatestOrderStatus
        )
        SELECT  @p1~ ,
                0 ,
                0 ,
                NULL
        WHERE   NOT EXISTS ( SELECT 1
                             FROM   dbo.Customers AS c
                             WHERE  CustomerName = @p1 )

此時沒有事務,上述雖然看起來很好不會引發死鎖可是對於插入操做會致使阻塞。我看到上述dudu老大提出的問題有以下答案:

BEGIN TRANSACTION
IF NOT EXISTS(SELECT 1 FROM [Relations] WITH(XLOCK,ROWLOCK) WHERE [UserId]=@UserId AND [RelativeUserId]=@RelativeUserId AND IsActive=1)
BEGIN
    INSERT INTO [Relations]([UserId], [RelativeUserId]) 
    VALUES (@UserId,@RelativeUserId)
    UPDATE [Users] SET FollowingCount=FollowingCount+1 WHERE UserID=@UserId
    UPDATE [Users] SET FollowerCount=FollowerCount+1 WHERE UserID=@RelativeUserId
END
COMMIT TRANSACTION

經查資料顯示對於XLOCK,SQL Server優化引擎極有可能忽略XLOCK提示,而致使沒法解決問題,具體未通過驗證。在這裏我以爲應該使用UPDLOCK更新鎖。經過UPDLOCK與其餘更新鎖不兼容,經過UPDLOCK來序列化整個過程,當運行第二個進程時,因爲第一個進程佔用鎖致使阻塞,因此直到第一個進程完成整個過程第二個進程都將處於阻塞狀態,因此對於dudu老大提出的問題是否最終改形成以下操做呢。

BEGIN TRANSACTION
IF NOT EXISTS(SELECT 1 FROM [Relations] WITH (ROWLOCK, UPDLOCK) WHERE [UserId]=@UserId AND [RelativeUserId]=@RelativeUserId AND IsActive=1) UPDATE [Users] SET FollowingCount=FollowingCount+1 WHERE UserID=@UserId; UPDATE [Users] SET FollowerCount=FollowerCount+1 WHERE UserID=@RelativeUserId; ELSE INSERT INTO [Relations]     ( [UserId],     [RelativeUserId]  ) VALUES ( @UserId,      @RelativeUserId   );
COMMIT TRANSACTION

總結 

本節咱們比較詳細講解了SQL Server中的死鎖以及避免死鎖的簡單介紹,對於如何避免死鎖咱們能夠從如下來看。

(1)鎖保持的時間越長,增長了死鎖的可能性,儘可能縮短事務的時間即儘可能使事務簡短。

(2)事務不要交叉進行,按照順序執行。

(3)對於有些邏輯可能不可避免須要交叉進行事務,此時咱們可能經過良好的索引設計來規避死鎖發生。

(4)咱們也能夠經過try..catch,捕獲事務出錯並retry。

(5)最後則是經過設置隔離級別來減小死鎖頻率發生。

好了本文到此結束,內容貌似有點冗長,沒辦法,太多須要學習,對於SQL Server基礎系列可能還剩下最後一節內容,那就是各類鎖的探討,咱們下節再會,see u。

相關文章
相關標籤/搜索