在系統設計過程當中,系統的穩定性、響應速度和讀寫速度相當重要,就像12306.cn那樣,固然咱們能夠經過提升系統併發能力來提升系統性能整體性能,但在併發做用下也會出現一些問題,例如死鎖。sql
今天的博文將着重介紹死鎖的緣由和解決方法。數據庫
定義:緩存
死鎖是因爲併發進程只能按互斥方式訪問臨界資源等多種因素引發的,而且是一種與執行時間和速度密切相關的錯誤現象。服務器
死鎖的定義:若在一個進程集合中,每個進程都在等待一個永遠不會發生的事件而造成一個永久的阻塞狀態,這種阻塞狀態就是死鎖。session
死鎖產生的必要條件:多線程
1.互斥mutual exclusion):系統存在着臨界資源;併發
2.佔有並等待(hold and wait):已經獲得某些資源的進程還能夠申請其餘新資源;app
3.不可剝奪(no preemption):已經分配的資源在其宿主沒有釋放以前不容許被剝奪;svg
4.循環等待(circular waiting):系統中存在多個(大於2個)進程造成的封閉的進程鏈,鏈中的每一個進程都在等待它的下一個進程所佔有的資源;工具
圖1死鎖產生條件
咱們知道哲學家就餐問題是在計算機科學中的一個經典問題(併發和死鎖),用來演示在並行計算中多線程同步(Synchronization)時產生的問題,其中一個問題就是存在死鎖風險。
圖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資源,並且它們都不釋放本身擁有的資源,這時資源和進程之間造成了一個環從而造成死鎖。
圖3死鎖(圖片源於wiki)
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而且插入了數據。
圖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使事務執行產生延時。
圖5事務執行結果
運行完上面的兩個查詢後,咱們發現其中一個事務執行失敗,系統提示該進程和另外一個進程發生死鎖,而另外一個事務執行成功,這是因爲SQL Server自動選擇一個事務做爲死鎖犧牲品。
既然發生了死鎖,那麼到底是哪一個資源被鎖定了呢?如今咱們經過死鎖排除方法一來查看具體是哪一個資源被鎖定。
如今咱們從新執行事務A和B,接着使用死鎖排除方法一查看更新事務具體使用到的鎖。
圖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),因爲行排他鎖和行排他鎖是衝突的因此致使死鎖。
圖7鎖的兼容性
前面咱們介紹了使用sp_lock查看更新操做時SQL Server使用的鎖(行鎖、頁鎖和表鎖),如今咱們在更新操做後查詢操做,SQL代碼以下:
如今咱們使用SQL Server Profiler分析死鎖
在本節中,咱們將看到如何使用SQL Server Profiler來捕獲死鎖跟蹤。
1.啓動SQL Server事件探查器和鏈接所需的SQL Server實例
2.建立一個新的跟蹤
3.在事件選擇頁中,取消默認事件選項,咱們選擇「死鎖圖形」事件、 「鎖定:死鎖」和「鎖定:死鎖鏈」以下圖所示:
4. 啓動一個新的跟蹤
5.在SSMS中,開兩個查詢窗口#1和#2,咱們從新執行前面兩個事務
6.事務執行結束,一個執行成爲,另外一個發生死鎖錯誤
7.咱們打開事件探查器,以下圖所示:
圖9 Deadlock graph
8.選擇Deadlock graph,咱們能夠直觀查看到兩個事務之間發生死鎖的緣由
圖10 事務進程A
上圖的橢圓形有一個叉,表示事務A被SQL Server選擇爲死鎖犧牲品,若是咱們把鼠標指針移動到橢圓中會出現一個提示。
圖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。
本文主要介紹了什麼是死鎖、怎樣致使了死鎖和死鎖的解決方法,正如咱們能夠看到,因爲致使死鎖的緣由不少,因此死鎖的解決方法不盡相同,首先咱們必須明確死鎖發生的地方,例如進程爲了爭奪哪類資源致使死鎖的,這時咱們能夠考慮使用Profiler工具進行跟蹤查詢;在清楚死鎖發生的地方後,咱們要檢查一下查詢是否考慮周到了,能夠根據以上的方法優化查詢語句。
參考
http://msdn.microsoft.com/zh-cn/library/ms174313.aspx
http://www.cnblogs.com/happyhippy/
[做者]: JK_Rush [出處]: http://www.cnblogs.com/rush/