1、基本原理
所謂“死鎖”,在操作系統的定義是:在一組進程中的各個進程均占有不會釋放的資源,但因互相申請被其他進程所站用不會釋放的資源而處於的一種永久等待狀態。
定義比較抽象,下圖可以幫助你比較直觀的理解死鎖:
出現死鎖需要滿足幾個必要條件:
a)互斥:進程獨占資源,資源不共享;
b)請求與保持:已經得到資源的進程可以再次申請新資源;
c)不剝奪:已分配的資源不能被其它進程強制剝奪;
d)環路等待:幾個進程組成環路,都在相互等待正被占用的資源;
對應到SQL Server中,在2個或多個任務中(insert、update、delete、select、alter table或Tran事務等等),如果每個任務鎖定了其它任務想要鎖定的資源,會造成這些任務永久阻塞,從而出現死鎖。這些資源可能是:單行數據(RID、HEAP堆中的行)、索引中的鍵(KEY,行鎖)、頁(Page,8KB)、區(Extent,8個連續頁)、堆或B樹、表(Table,數據和索引)、文件(File,數據庫文件)、整個數據庫(DataBase)。
如果系統中的資源不足或者資源分配策略不當,會導致因進程間的資源爭用產生死鎖現象。但更多的可能是程序員的程序有問題。“鎖”有多種方式,如意向鎖、共享鎖、排他鎖等等。鎖還有多種粒度,如行鎖、表鎖。
了解了死鎖產生的原因,就可以最大可能的避免與預防死鎖。只要上述4個必要條件中有1個不滿足,就不會發生死鎖。所以,在系統設計、實現階段就可以在資源分配與占用、資源訪問順序等方面采取必要措施。
2、一個例子
直面死鎖,來看一個例子:如圖1所示,新建一個查詢窗口,並利用事務的原子特性和update語句的排他鎖特性把2個表中的記錄鎖住;如圖2所示,再次新建一個查詢窗口,2條很簡單的SQL語句長時間仍沒有執行完成。
3、檢測與排查
3.1 通過Profile工具看死鎖
Profile是SQL Server自帶的跟蹤分析工具,開啟Profile來捕捉死鎖信息可以更直觀的看到相關信息。
3.2 通過系統存儲過程看死鎖
sp_who和sp_lock是SQL Server的2個系統存儲過程,可以用它們來查詢數據庫中的鎖情況。sp_who提供有關的數據庫實例中當前用戶、會話和進程的信息,如下圖,我們看到spid=56的會話(UPDATE語句)被spid=54的會話阻塞。
sp_lock提供有關鎖的信息,如下圖。我們可以通過spid知道是哪個會話鎖住了資源,可以通過ObjId知道被鎖住的資源是什么。
執行如下SQL腳本獲取被鎖資源和資源所屬的數據庫:
SELECT OBJECT_NAME(421666738) AS LockedResource,DB_NAME(11) AS DBName; -------------------------------------------------------------- LockedResource DBName -------------------------------------------------------------- tb_TE_SizeInformation JYBGDB
執行如下腳本獲取鎖資源的會話正在執行的SQL腳本:
DBCC INPUTBUFFER(54); --------------------------------------------------------------------------- EventInfo EventType Parameters --------------------------------------------------------------------------- --根據事務的原子性實現個必要條件中 Language Event 0 --請求和等待 BEGIN TRAN --update語句在數據行上加排他鎖 --和其它所有鎖不兼容 --實現個必要條件中的:互斥 UPDATE tb_TE_BrandInformation SET IsCompensate=0 UPDATE tb_TE_SizeInformation SET [Description]=''
4、處理方式
4.1 SQL Server自動處理
“無為而治”。當數據庫產生死鎖時,SQL Server通過一個叫“鎖監視器”的東西捕獲死鎖信息,並根據一定的規則自動選擇一個SQL作為鎖的犧牲品,並返回如下報錯信息:
服務器: 消息 1205,級別 13,狀態 50,行 1 |
如果你對數據庫還不夠了解,那建議你向其他有經驗的人求助,在此之前不要輕易對數據庫進行修改。
4.2 Kill會話
通過3.2中提到的系統存儲過程可以獲取到與死鎖相關的信息。可以查詢其中是哪個spid導致的死鎖,並使用Kill spid的方法把它干掉。但是這只能是一種臨時的解決方案,我們不可能一遇到死鎖就在用戶的生產環境里排查死鎖、Kill sp。同樣的道理,也不可能一遇到死鎖就重啟SQL Server服務,甚至重啟數據庫服務器。
SQL腳本:
Kill 54; --此處54即分析后得到的spid值 |
4.3 設定鎖請求超時
默認情況下,數據庫沒有鎖定超時期限。也就是說一個會話在申請新的資源時,如果這個資源已經被其它進程鎖定,那么本會話會一直處於等待狀態。這樣無疑是有問題的。我們可以通過SQL命令來設定鎖請求超時。也可以訪問全局變量 @@LOCK_TIMEOUT 來查看這個值。
SET LOCK_TIMEOUT 20000; --單位是毫秒 |
當請求鎖超過設定時間時,SQL Server將返回錯誤。我們的程序可以根據返回的錯誤來進行響應的處理,避免長時間的用戶等待。
服務器: 消息 1222,級別 16,狀態 50,行 1 已超過了鎖請求超時時段。 |
當然,使用這種方式來處理所有的鎖請求是不合適的,也是不負責任的。在多數情況下是我們的程序的設計、實現的問題導致了死鎖。在處理過程中,我們既要治標,更要治本。
4.4 修改程序
在3.2的最后,我們通過系統存儲過程和幾個命令找到了鎖定資源的SQL命令。以這次LL項目為例,我們發現是WEB管理系統上的一個統計報表(SELECT)在執行過程中長時間的那一個生產信息表鎖定,導致現場各機台上位機系統想要插入新的生產記錄(INSERT)時長時間等待。所以在現場項目組每次重新啟動SQL Server服務或者重啟數據庫服務器2個小時以后,這個問題依然重復出現。
這個時候如果采用Kill掉這個統計報表請求的方式處理,結果和重啟SQL Server服務、重啟數據庫服務器沒有區別,2個小時后問題依舊。如果采用設定鎖請求超時的方式處理,那么這個統計報表每次執行都不會獲得想要的結果,而且每次執行也會鎖定一定的時間導致現場上位機的等待。
這次我們的處理措施是:1)暫時禁用了WEB管理系統上的這個報表功能;2)重啟了SQL Server服務;3)優化報表的SQL語句;4)啟用報表功能。之后的一段時間沒有再次出現這樣的問題。
通過對這個報表的性能優化,這個問題算是解決的差不多了。但是經過事后了解,發現報表的性能問題並不在於開發人員的疏忽或水平不夠。問題的根本在這個生產信息表的設計有問題。在一個數據量達到1000w級的表中,我們采用char(10)來保存日期值,雖然INSERT、UPDATE、DELETE時沒有問題,但是在執行SELECT且這個日期值字段作為過濾條件時發生性能問題是必然的。經過測試,這個字段的數據類型改為datetime時的執行時間不到性能優化后的10%。
所以,不但是在開發階段,早在設計階段就已經有了性能隱患。
4.5 升級硬件
不贅述。
5、如何預防
首先要理解,在多並發的環境中死鎖是不可避免的,只能通過合理的數據庫設計、良好的索引、適當的查詢語句以及隔離等級等措施盡量減少死鎖。
最開始列出了死鎖的4個必要條件,只要想辦法破壞任意1個或多個條件就可以避免產生死鎖。下列方法有助於最大限度的降低死鎖:
a) 按同一順序訪問對象;
b)避免事務中的用戶交互,也就是在事務執行過程中不要包含用戶交互的步驟;
c)保持事務簡短並在一個批處理中;
d)SELECT語句加WITH(NOLOCK)提示;
SELECT * FROM TABLE1 WITH(NOLOCK); SELECT * FROM TABLE2 WITH(NOLOCK); |
這種寫法在執行中不對查詢到的資源加鎖,就允許2條SQL可以並發地訪問同一資源。但是加WITH(NOLOCK)提示可能會導致臟讀!!!
e)使用較低的隔離級別;
暫不需要了解,不贅述。
f)使用綁定連接;
處理程序端的死鎖,非數據庫端,不贅述。
6、結束語
項目實施過程中遇到死鎖現象在所難免。通過前面的介紹,希望大家能夠對它有一個比較簡單的認識,在遇到異常情況的時候不至於束手無策。如果以上內容有什么技術上不對的問題或觀點,歡迎大家直接向我提出來一起研究溝通,也歡迎大家在遇到其它數據庫方面的問題時能和我一起探討,共同提高。