Deadlock的一些總結


1.1.1 摘要

      在系統設計過程中,系統的穩定性、響應速度和讀寫速度至關重要,就像12306.cn那樣,當然我們可以通過提高系統並發能力來提高系統性能總體性能,但在並發作用下也會出現一些問題,例如死鎖。

     今天的博文將着重介紹死鎖的原因和解決方法。

1.1.2 正文

      定義:

      死鎖是由於並發進程只能按互斥方式訪問臨界資源等多種因素引起的,並且是一種與執行時間和速度密切相關的錯誤現象。

      死鎖的定義:若在一個進程集合中,每一個進程都在等待一個永遠不會發生的事件而形成一個永久的阻塞狀態,這種阻塞狀態就是死鎖。

      死鎖產生的必要條件:

      1.互斥mutual exclusion):系統存在着臨界資源;

      2.占有並等待(hold and wait):已經得到某些資源的進程還可以申請其他新資源;

      3.不可剝奪(no preemption):已經分配的資源在其宿主沒有釋放之前不允許被剝奪;

      4.循環等待(circular waiting):系統中存在多個(大於2個)進程形成的封閉的進程鏈,鏈中的每個進程都在等待它的下一個進程所占有的資源;

deadlock1

圖1死鎖產生條件

      我們知道哲學家就餐問題是在計算機科學中的一個經典問題(並發和死鎖),用來演示在並行計算中多線程同步(Synchronization)時產生的問題,其中一個問題就是存在死鎖風險。

clip_image002

圖2哲學家就餐問題(圖片源於wiki)

     而對應到數據庫中,當兩個或多個任務中,如果每個任務鎖定了其他任務試圖鎖定的資源,此時會造成這些任務阻塞,從而出現死鎖;這些資源可能是:單行(RID,堆中的單行)、索引中的鍵(KEY,行鎖)、頁(PAG,8KB)、區結構(EXT,連續的8頁)、堆或B樹(HOBT) 、表(TAB,包括數據和索引)、文件(File,數據庫文件)、應用程序專用資源(APP)、元數據(METADATA)、分配單元(Allocation_Unit)、整個數據庫(DB)。

     假設我們定義兩個進程P1和P2,它們分別擁有資源R2和R1,但P1需要額外的資源R1恰好P2也需要R2資源,而且它們都不釋放自己擁有的資源,這時資源和進程之間形成了一個環從而形成死鎖。

 deadlock4.svg

圖3死鎖(圖片源於wiki)

SQL Server中死鎖排查:

      1.使用SQL Server中系統存儲過程sp_who和sp_lock,可以查看當前數據庫中阻塞進程的情況;

      首先我們在數據庫中創建兩個表Who和Lock分別用來存放阻塞和鎖定的數據,SQL代碼如下:

CREATE Table Who(spid int,
    ecid int,
    status nvarchar(50),
    loginname nvarchar(50),
    hostname nvarchar(50),
    blk int,
    dbname nvarchar(50),
    cmd nvarchar(50),
    request_ID int);

CREATE Table Lock(spid int,
    dpid int,
    objid int,
    indld int,
    [Type] nvarchar(20),
    Resource nvarchar(50),
    Mode nvarchar(10),
    Status nvarchar(10)
);

      接着我們要把阻塞和鎖定數據分別存放到Who和Lock表中,SQL代碼如下:

INSERT INTO Who
    -- Diagnose which process causing the block.
    EXEC sp_who active
INSERT INTO Lock
    -- Check which source has been locked.
    EXEC sp_lock

DECLARE @DBName nvarchar(20);
SET @DBName='YourDatabaseName'

SELECT Who.* FROM Who WHERE dbname=@DBName
SELECT Lock.* FROM Lock
    JOIN Who
        ON Who.spid=Lock.spid
            AND dbname=@DBName;

-- Displays the last statement sent from 
-- a client to an instance of Microsoft SQL Server.
DECLARE crsr Cursor FOR
    SELECT blk FROM Who WHERE dbname=@DBName AND blk<>0;
DECLARE @blk int;
open crsr;
FETCH NEXT FROM crsr INTO @blk;
WHILE (@@FETCH_STATUS = 0)
BEGIN;
    dbcc inputbuffer(@blk);
    FETCH NEXT FROM crsr INTO @blk;
END;
close crsr;
DEALLOCATE crsr;

-- Get the locked source.
SELECT Who.spid,hostname,objid,[type],mode,object_name(objid) as objName FROM Lock
    JOIN Who
        ON Who.spid=Lock.spid
            AND dbname=@DBName
    WHERE objid<>0;

      2.使用SQL Server Profiler分析死鎖,將Deadlock graph事件類添加到跟蹤。此事件類使用死鎖涉及到的進程和對象的XML數據填充跟蹤中的TextData數據列。SQL Server 事件探查器可以將XML文檔提取到死鎖XML(.xdl) 文件中,以后可在SQL Server Management Studio中查看該文件(下面將給出詳細介紹)。

死鎖的示例和解決方法

      首先我們在數據庫tempdb中創建兩個表DlTable1和DlTable2,它們都包含兩個字段分別是Id和Name,接着我們往這兩個表中插入數據,具體SQL代碼如下:

-- Note we use tempdb for testing.
USE tempdb

-- Create datatable in tempdb.
CREATE TABLE DlTable1 (DL1Id INT, DL1Name VARCHAR(20))
CREATE TABLE DlTable2 (DL2Id INT, DL2Name VARCHAR(20))


-- Insert multiple data into DlTable1 and DlTable2 in SQL Server 2005.
INSERT INTO DlTable1
SELECT 1, 'Deadlock'
UNION ALL
SELECT 2, 'JKhuang'
UNION ALL
SELECT 3, 'Test'
GO

INSERT INTO DlTable2
SELECT 1, 'Deadlock'
UNION ALL
SELECT 2, 'JacksonHuang'
UNION ALL
SELECT 3, 'Test'
GO


-- Insert multiple data into DlTable1 and DlTable2 in SQL Server 2008.
INSERT INTO DlTable1 VALUES (1, 'Deadlock'), (2, 'JKhuang'), (3, 'Test')
INSERT INTO DlTable2 VALUES (1, 'Deadlock'), (2, 'JacksonHuang'), (3, 'Test')

    現在我們執行以上SQL代碼成功創建了DlTable1和DlTable2並且插入了數據。

deadlock5

圖4插入數據到表中

    接着我們打開兩個查詢窗口分別創建兩個獨立的事務A和B如下:

-- In query window 1.
USE tempdb
GO

-- Create transaction A.
BEGIN     TRANSACTION
            UPDATE DlTable1 SET DL1Name = 'Uplock' WHERE DL1Id = 2
            -- Delay 23 second.
            WAITFOR DELAY '00:00:23'
            UPDATE DlTable2 SET DL2Name = 'Downlock' WHERE DL2Id = 2
ROLLBACK TRANSACTION

-- In query window 2.
USE tempdb
GO

-- Create transaction B.
BEGIN     TRANSACTION
            UPDATE DlTable2 SET DL2Name = 'Downlock' WHERE DL2Id = 2
            -- Delay 23 second.
            WAITFOR DELAY '00:00:23'
            UPDATE DlTable1 SET DL1Name = 'Uplock' WHERE DL1Id = 2
ROLLBACK TRANSACTION

     上面我們定義了兩個獨立的事務A和B,為了測試死鎖這里我們使用WAITFOR DELAY使事務執行產生延時。

deadlock13

deadlock10

圖5事務執行結果

      運行完上面的兩個查詢后,我們發現其中一個事務執行失敗,系統提示該進程和另一個進程發生死鎖,而另一個事務執行成功,這是由於SQL Server自動選擇一個事務作為死鎖犧牲品。

      既然發生了死鎖,那么究竟是哪個資源被鎖定了呢?現在我們通過死鎖排除方法一來查看具體是哪個資源被鎖定。

      現在我們重新執行事務A和B,接着使用死鎖排除方法一查看更新事務具體使用到的鎖。

deadlock14

圖6更新操作使用的鎖

      通過上圖我們知道,首先事務A給表DlTable1下了行排他鎖(RID X),然后在下頁意向更新鎖(PAG IX),最后給整個DlTable1表下了表意向更新鎖(TAB IX);事務B的使用的鎖也是一樣的。

      事務A擁有DL1Id = 2行排他鎖(RID X)同時去請求DL2Id = 2的行排他鎖(RID X),但我們知道事務B已經擁有DL2Id = 2的行排他鎖(RID X),而且去請求DL1Id = 2行排他鎖(RID X),由於行排他鎖和行排他鎖是沖突的所以導致死鎖。

deadlock15

圖7鎖的兼容性

      前面我們介紹了使用sp_lock查看更新操作時SQL Server使用的鎖(行鎖、頁鎖和表鎖),現在我們在更新操作后查詢操作,SQL代碼如下:

      現在我們使用SQL Server Profiler分析死鎖

      在本節中,我們將看到如何使用SQL Server Profiler來捕獲死鎖跟蹤。

      1.啟動SQL Server事件探查器和連接所需的SQL Server實例

      2.創建一個新的跟蹤

      3.在事件選擇頁中,取消默認事件選項,我們選擇“死鎖圖形”事件、 “鎖定:死鎖”和“鎖定:死鎖鏈”如下圖所示:

deadlock8

圖8事件選擇設置

     4. 啟動一個新的跟蹤

     5.在SSMS中,開兩個查詢窗口#1和#2,我們重新執行前面兩個事務

     6.事務執行結束,一個執行成為,另一個發生死鎖錯誤

     7.我們打開事件探查器,如下圖所示:

deadlock11

圖9 Deadlock graph

     8.選擇Deadlock graph,我們可以直觀查看到兩個事務之間發生死鎖的原因

deadlock17

圖10 事務進程A

      上圖的橢圓形有一個叉,表示事務A被SQL Server選擇為死鎖犧牲品,如果我們把鼠標指針移動到橢圓中會出現一個提示。

deadlock16

圖11 事務進程B

      上圖的橢圓形表示進程執行成功,我們把鼠標指針移動到橢圓中也會出現一個提示。

      中間的兩個矩形框稱為資源節點,它們代表的數據庫對象,如表,行或索引。由於事務A和B在擁有各自資源時試圖獲得對方資源的一個獨占鎖,使得進程相互等待對方釋放資源從而導致死鎖。

死鎖避免:

     現在讓我們回顧一下上了死鎖的四個必要條件:互斥,占有並等待,不可剝奪和循環等待;我們只需破壞其中的一個或多個條件就可以避免死鎖發生,方法如下:

     (1).按同一順序訪問對象。(注:避免出現循環,降低了進程的並發執行能力)

     (2).避免事務中的用戶交互。(注:減少持有資源的時間,減少競爭)

     (3).保持事務簡短並處於一個批處理中。(注:同(2),減少持有資源的時間)

     (4).使用較低的隔離級別。(注:使用較低的隔離級別(例如已提交讀)比使用較高的隔離級別(例如可序列化)持有共享鎖的時間更短,減少競爭)

     (5).使用基於行版本控制的隔離級別:2005中支持快照事務隔離和指定READ_COMMITTED隔離級別的事務使用行版本控制,可以將讀與寫操作之間發生的死鎖幾率降至最低:

     SET ALLOW_SNAPSHOT_ISOLATION ON --事務可以指定 SNAPSHOT 事務隔離級別;

     SET READ_COMMITTED_SNAPSHOT ON --指定 READ_COMMITTED 隔離級別的事務將使用行版本控制而不是鎖定。默認情況下(沒有開啟此選項,沒有加with nolock提示),SELECT語句會對請求的資源加S鎖(共享鎖);而開啟了此選項后,SELECT不會對請求的資源加S鎖。

      注意:設置 READ_COMMITTED_SNAPSHOT選項時,數據庫中只允許存在執行 ALTER DATABASE命令的連接。在 ALTER DATABASE完成之前,數據庫中決不能有其他打開的連接。數據庫不必一定要處於單用戶模式中。

     在數據庫中設置READ COMMITTED SNAPSHOT 或 ALLOW SNAPSHOT ISOLATIONON ON時,查詢數據時不再使用請求共享鎖,如果請求的行正被鎖定(例如正在被更新),SQL_Server會從行版本存儲區返回最早的關於該行的記錄(SQL_server會在更新時將之前的行數據在tempdb庫中形成一個鏈接列表。(詳細請點這里這里

ALTER Database DATABASENAME SET READ_COMMITTED_SNAPSHOT ON

     (6).使用綁定連接。(注:綁定會話有利於在同一台服務器上的多個會話之間協調操作。綁定會話允許一個或多個會話共享相同的事務和鎖(但每個回話保留其自己的事務隔離級別),並可以使用同一數據,而不會有鎖沖突。可以從同一個應用程序內的多個會話中創建綁定會話,也可以從包含不同會話的多個應用程序中創建綁定會話。在一個會話中開啟事務(begin tran)后,調用exec sp_getbindtoken @Token out;來取得Token,然后傳入另一個會話並執行EXEC sp_bindsession @Token來進行綁定(最后的示例中演示了綁定連接)。

解決死鎖

      這里有幾個方法可以幫助我們解決死鎖問題。

      優化查詢

      我們在寫查詢語句時,要考慮一下查詢是否Join了沒有必要的表?是否返回數據太多(太多的列或行)?查詢是否執行表掃描?是否能通過調整查詢次序來避免死鎖?是否應該使用Join的地方使用了Left Join?Not In語句是否考慮周到?

      我們在寫查詢語句可以根據以上准則來考慮查詢是否應該做出優化。

      慎用With(NoLock)

      默認情況下SELECT語句會對查詢到的資源加S鎖(共享鎖),由於S鎖與X鎖(排他鎖)不兼容,在加上With(NoLock)后,SELECT不對查詢到的資源加鎖(或者加Sch-S鎖,Sch-S鎖可以與任何鎖兼容);從而使得查詢語句可以更好和其他語句並發執行,適用於表數據更新不頻繁的情況。

     也許有些人會提出質疑With(NoLock),可能會導致臟讀,首先我們要考慮查詢的表是否頻繁進行更新操作,而且是否要讀回來的數據會被修改,所以衡量是否使用With(NoLock)還是要根據具體實際出發。

     優化索引

     是否有任何缺失或多余的索引?是否有任何重復的索引?

     處理死鎖

     我們不能時刻都觀察死鎖的發生,但我們可以通過日志來記錄系統發生的死鎖,我們可以把系統的死鎖錯誤寫入到表中,從而方便分析死鎖原因。

     緩存

     也許我們正在執行許多相同的查詢非常頻繁,如果我們把這些頻繁的操作都放到Cache中,執行查詢的次數將減少發生死鎖的機會。我們可以在數據庫的臨時表或表,或內存,或磁盤上應用Cache。

1.1.3 總結

      本文主要介紹了什么是死鎖、怎樣導致了死鎖和死鎖的解決方法,正如我們可以看到,由於導致死鎖的原因很多,所以死鎖的解決方法不盡相同,首先我們必須明確死鎖發生的地方,例如進程為了爭奪哪類資源導致死鎖的,這時我們可以考慮使用Profiler工具進行跟蹤查詢;在清楚死鎖發生的地方后,我們要檢查一下查詢是否考慮周到了,可以根據以上的方法優化查詢語句。

參考

http://msdn.microsoft.com/zh-cn/library/ms174313.aspx

http://www.simple-talk.com/sql/learn-sql-server/how-to-track-down-deadlocks-using-sql-server-2005-profiler/

http://www.cnblogs.com/happyhippy/


免責聲明!

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



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