數據庫中鎖機制的學習

 
       我們在做很多項目時都要涉及到數據庫,特別是一些比較大型的web項目,更是有較大的併發處理,所以對數據庫的操作有可能會產生死鎖,對於數據庫的死鎖,一般數據庫系統都會有一套機制去解鎖,一般不會造成數據庫的癱瘓,但解鎖的過程會造成數據庫性能的急速下降,反映到程序上就會造成程序的反應性能的下降,並且會造成程序有的操作失敗。雖然一般對於數據庫級別的鎖定於解鎖程序員不會在程序中用代碼編程去處理,但是對於其的瞭解對於程序員來說還是很有好處的,它能讓我們在出現問題時快速找到問題的原因,同時讓我們在編程時注意哪些可能造成數據庫死鎖的不太好的編碼。
       以下以sql server2000爲例,來了解數據庫死鎖的一些知識(其實各種大型數據庫所採用的鎖的基本理論是一致的,但在具體實現上各有差別。)
        之所以要有鎖的概念是因爲存在着多用戶同時需要訪問數據庫,如果沒有鎖定且多個用戶同時訪問一個數據庫,則當他們的事務同時使用相同的數據時可能會發生問題,這些問題包括:丟失更新、髒讀、不可重複讀和幻覺讀等等,在這就不一一解釋了,總之就是發生數據不一致現象。
       在sql server2000中鎖是具有粒度的,即可以對不同的資源加鎖。鎖定在較小的粒度的資源(例如行)上可以增加系統的併發量但需要較大的系統開銷,從而也會影響系統的性能,因爲鎖定的粒度較小則操作可能產生的鎖的數量會增加;鎖定在較大的粒度(例如表)就併發而言是相當昂貴的,因爲鎖定整個表限制了其它事務對錶中任意部分進行訪問,但要求的開銷較低,因爲需要維護的鎖較少,所以在這裏是一種互相制約的關係。
        Sql server2000中鎖定的粒度包括 行、頁、擴展盤區、表、庫等資源。實際上這些粒度大多可以看成是sql server系統的空間管理的不同,在SQL Server 2000系統中,最小的空間管理單位是頁,一個頁有8K。所有的數據、日誌、索引都存放在頁上。另外,使用頁有一個限制,這就是表中的一行數據必須在同一個頁上,不能跨頁。頁上面的空間管理單位是盤區,一個盤區是8個連續的頁。表和索引的最小佔用單位是盤區。數據庫是由一個或者多個表或者索引組成,即是由多個盤區組成。放在一個表上的鎖限制對整個表的併發訪問;放在盤區上的鎖限制了對整個盤區的訪問;放在數據頁上的鎖限制了對整個數據頁的訪問;放在行上的鎖只限制對該行的併發訪問。
        在sql server2000中鎖的加載和釋放一般是有數據庫系統自動完成,並且在sql server中,還支持鎖升級(lock escalation)。所謂鎖升級是指調整鎖的粒度,將多個低粒度的鎖替換成少數的更高粒度的鎖,以此來降低系統負荷。
        在SQL Server數據庫中加鎖時,除了可以對不同的資源加鎖,即鎖具有不同粒度,還可以使用不同程度的加鎖方式,即鎖有多種模式,SQL Server中鎖模式包括:
1.       共享鎖 SQL Server中,共享鎖用於所有的只讀數據操作。共享鎖是非獨佔的,允許多個併發事務讀取其鎖定的資源。
2.         更新鎖 更新鎖在修改操作的初始化階段用來鎖定可能要被修改的資源,這樣可以避免使用共享鎖造成的死鎖現象。因爲使用共享鎖時,修改數據的操作分爲兩步,首先獲得一個共享鎖,讀取數據,然後將共享鎖升級爲排它鎖,然後再執行修改操作。這樣如果同時有兩個或多個事務同時對一個事務申請了共享鎖,在修改數據的時候,這些事務都要將共享鎖升級爲排它鎖。這時,這些事務都不會釋放共享鎖而是一直等待對方釋放,這樣就造成了死鎖。如果一個數據在修改前直接申請更新鎖,在數據修改的時候再升級爲排它鎖,就可以避免死鎖。
3
.排它鎖 排它鎖是爲修改數據而保留的。它所鎖定的資源,其他事務不能讀取也不能修改
4.結構鎖 執行表的數據定義語言 (DDL) 操作(例如添加列或除去表)時使用架構修改 (Sch-M) 鎖。
5.意向鎖 意向鎖說明SQL Server有在資源的低層獲得共享鎖或排它鎖的意向。
6.大容量更新鎖 當將數據大容量複製到表,且指定了 TABLOCK 提示或者使用 sp_tableoption 設置了 table lock on bulk 表選項時,將使用大容量更新 鎖。大容量更新鎖允許進程將數據併發地大容量複製到同一表,同時防止其它不進行大容量複製數據的進程訪問該表。 
       在sql server除了由系統自動的加鎖外,也可以靠寫sql語句來手動的加鎖(顯示加鎖)。我們可以使用 SELECT、INSERT、UPDATE 和 DELETE 語句指定表級鎖定提示的範圍,以引導 Microsoft SQL Server 2000 使用所需的鎖類型。當需要對對象所獲得鎖類型進行更精細控制時,使用表級鎖定提示更改默認的鎖定行爲。所指定的表級鎖定提示有如下幾種:
1. HOLDLOCK: 在該表上保持共享鎖,直到整個事務結束,而不是在語句執行完立即釋放所添加的鎖。
2. NOLOCK:不添加共享鎖和排它鎖,當這個選項生效後,可能讀到未提交讀的數據或“髒數據”,這個選項僅僅應用於SELECT語句。
3. PAGLOCK:指定添加頁鎖(否則通常可能添加表鎖)。
4. READCOMMITTED用與運行在提交讀隔離級別的事務相同的鎖語義執行掃描。默認情況下,SQL Server 2000 在此隔離級別上操作。。
5. READPAST: 跳過已經加鎖的數據行,這個選項將使事務讀取數據時跳過那些已經被其他事務鎖定的數據行,而不是阻塞直到其他事務釋放鎖,READPAST僅僅應用於READ COMMITTED隔離性級別下事務操作中的SELECT語句操作。
6. READUNCOMMITTED:等同於NOLOCK
7. REPEATABLEREAD:設置事務爲可重複讀隔離性級別。
8. ROWLOCK:使用行級鎖,而不使用粒度更粗的頁級鎖和表級鎖。
9. SERIALIZABLE:用與運行在可串行讀隔離級別的事務相同的鎖語義執行掃描。等同於 HOLDLOCK
10. TABLOCK:指定使用表級鎖,而不是使用行級或頁面級的鎖,SQL Server在該語句執行完後釋放這個鎖,而如果同時指定了HOLDLOCK,該鎖一直保持到這個事務結束。
11. TABLOCKX:指定在表上使用排它鎖,這個鎖可以阻止其他事務讀或更新這個表的數據,直到這個語句或整個事務結束。
12. UPDLOCK :指定在讀表中數據時設置更新 鎖(update lock)而不是設置共享鎖,該鎖一直保持到這個語句或整個事務結束,使用UPDLOCK的作用是允許用戶先讀取數據(而且不阻塞其他用戶讀數據),並且保證在後來再更新數據時,這一段時間內這些數據沒有被其他用戶修改。 
       上面我們主要講了數據庫中鎖的分類和顯示加鎖的一些知識,下面講一講關於數據庫死鎖的問題。
        在數據庫系統中,死鎖是指多個用戶(進程)分別鎖定了一個資源,並又試圖請求鎖定對方已經鎖定的資源,這就產生了一個鎖定請求環,導致多個用戶(進程)都處於等待對方釋放所鎖定資源的狀態。還有一種比較典型的死鎖情況是當在一個數據庫中時,有若干個長時間運行的事務執行並行的操作,當查詢分析器處理一種非常複雜的查詢例如連接查詢時,那麼由於不能控制處理的順序,有可能發生死鎖現象。
例如分別同時執行下面的兩個事務
A:
DECLARE @au_id varchar(11), @au_lname varchar(40)
SELECT @au_id = '111-11-1111', @au_lname = 'test1'
BEGIN TRANSACTION
INSERT Authors VALUES
 (@au_id, @au_lname, '', '', '', '', '', '11111', 0)
WAITFOR DELAY '00:00:05'
SELECT *
 FROM authors
 WHERE au_lname LIKE 'Test%'
COMMIT
B:
DECLARE @au_id varchar(11), @au_lname varchar(40)
SELECT @au_id = '111-11-1112', @au_lname = 'test2'
BEGIN TRANSACTION
INSERT Authors VALUES
 (@au_id, @au_lname, '', '', '', '', '', '11111', 0)
WAITFOR DELAY '00:00:05'
SELECT *
 FROM authors
 WHERE au_lname LIKE 'Test%'
COMMIT
則數據庫就會出現死鎖。原因在於在 5 秒鐘內同時執行A和B。每個事務都要等待至少 5 秒鐘的時間才能發出 SELECT 語句,所有每個連接都將完成 INSERT 操作,這樣就保證了兩個事務中的 INSERT 操作在各自的SELECT 語句發佈前就已經完成了。每個事務中的 SELECT 語句都嘗試讀取 authors 表格中的所有數據,查找 au_lname 字段值中類似“Test%”格式的數據。因此,兩個事務中的 SELECT 語句都將嘗試讀取各自連接中的插入數據 — 也讀取對方連接中的插入數據。而READ COMMITTED 隔離級別通過發佈共享鎖確保 SELECT 語句永遠不讀取未提交的數據。對於同一個資源,共享鎖與排它鎖互不兼容,請求者在發佈共享鎖之前必須等待排它鎖釋放。每個連接對於插入的數據都設置了排它鎖,因此嘗試讀取對方插入數據的 SELECT 語句將試圖解除插入數據的共享鎖,但它會被阻塞。兩個連接將互相阻塞,從而形成一個死鎖。
       在SQL Server中,系統能夠自動定期搜索和處理死鎖問題。系統在每次搜索中標識所有等待鎖定請求的進程會話,如果在下一次搜索中該被標識的進程仍處於等待狀態,SQL Server就開始遞歸死鎖搜索。當搜索檢測到鎖定請求環時,SQL Server 通過自動選擇可以打破死鎖的線程(死鎖犧牲品)來結束死鎖。SQL Server 回滾作爲死鎖犧牲品的事務,通知線程的應用程序(通過返回 1205 號錯誤信息),取消線程的當前請求,然後允許不間斷線程的事務繼續進行。SQL Server 通常選擇運行撤消時花費最少的事務的線程作爲死鎖犧牲品。另外,用戶可以使用 SET 語句將會話的 DEADLOCK_PRIORITY 設置爲 LOW。DEADLOCK_PRIORITY 選項控制在死鎖情況下如何衡量會話的重要性。如果會話的設置爲 LOW ,則當會話陷入死鎖情況時將成爲首選犧牲品。
        我們除了靠數據庫本身來處理死鎖外,我們在sql server2005中也可通過try/catch來處理產生的問題。以下爲msdn上關於這個問題的描述: 
        讓我們來使用 TRY/CATCH 語句修改代碼正文。(對於本示例,需要以 SQL Server 2005 版本運行代碼。)使用 TRY/CATCH 時,操作代碼和錯誤處理代碼是分開的。您應該將執行一個操作的代碼放在 TRY 語句塊中,將錯誤處理代碼放在 CATCH 語句塊中。如果 TRY 語句塊中的代碼執行失敗,代碼執行將跳到 CATCH 語句塊。(除了那些防礙整個批處理運行的錯誤(如,丟失對象),該方法幾乎適用於所有的錯誤。) 
       以下示例使用 TRY/CATCH 語句對前面使用的代碼進行了改寫。代碼標題相同,但是代碼正文不同: 
BEGIN TRANSACTION
BEGIN TRY
 INSERT Authors VALUES
 (@au_id, @au_lname, '', '', '', '', '', '11111', 0)
 WAITFOR DELAY '00:00:05'
 SELECT COUNT(*) FROM Authors
 COMMIT
END TRY
BEGIN CATCH
 SELECT ERROR_NUMBER() AS ErrorNumber
 ROLLBACK
END CATCH;
SELECT @@TRANCOUNT AS '@@Trancount' 
       現在,在連接到 SQL Server 2005 的並列窗口中運行這些代碼,在此之前您需要確認已經刪除了 authors 表格中任何可能阻止插入操作的數據;或者,您可以使用前置 DELETE 語句。 
       兩個窗口返回的 @@TRANCOUNT 級別都爲 0,這表明仍然發生了死鎖,但 TRY/CATCH 語句捕獲了這次發生的死鎖。死鎖犧牲品的批處理沒有再次中止,可在它的輸出結果中看到錯誤: 
ErrorNumber
-----------
1205
 
@@Trancount
-----------
0 
       您應該已經發現 TRY/CATCH 語句具有的威力了。因爲死鎖錯誤能夠爲 CATCH 語句塊所捕獲,所以批處理將不再中止,T-SQL 代碼也能繼續執行。對於死鎖犧牲品而言,死鎖錯誤 1205 將代碼放入 CATCH 語句塊 — 在這裏您可以使用新的錯誤處理函數瀏覽死鎖錯誤。前置代碼僅使用 ERROR_NUMBER() 函數取代 @@ERROR 變量,您也可以使用 ERROR_MESSAGE()、ERROR_PROCEDURE()、ERROR_SEVERITY() 和 ERROR_STATE()。這些函數的功能一目瞭然,它們提供的功能比我們以往使用的更多。 
       請注意,這個前置 CATCH 語句塊包含一個 ROLLBACK。這樣做的原因是,即使捕獲了死鎖錯誤,事務也不會回滾。事務仍然要失敗,但是,現在您有責任在 TRY/CATCH 語句中回滾事務。那麼,區別在哪裏?儘管您不能使事務繼續進行,但是您能夠重試事務! 
       在 SQL Server 2000 的 T-SQL 中,錯誤 1205 令人沮喪之處是它提供的建議:“Rerun the transaction.”問題是,至少在 SQL Server 2000 的 T-SQL 中,您不能做到這一點。但是,由於 SQL Server 2005 的 TRY/CATCH 爲我們提供了捕獲死鎖錯誤的方法,現在,重試事務是可能實現的。 
       以下代碼正文說明了一種執行重試操作的方法。這段代碼仍然使用與前面相同的標題: 
DECLARE @Tries tinyint
SET @Tries = 1
WHILE @Tries <= 3
BEGIN
 BEGIN TRANSACTION
 BEGIN TRY
    INSERT Authors VALUES
      (@au_id, @au_lname, '', '', '', '', '',
'11111', 0)
    WAITFOR DELAY '00:00:05'
    SELECT * FROM authors WHERE au_lname LIKE 'Test%'
    COMMIT
    BREAK
 END TRY
 BEGIN CATCH
    SELECT ERROR_NUMBER() AS ErrorNumber
    ROLLBACK
    SET @Tries = @Tries + 1
    CONTINUE
 END CATCH;
END 
       這段代碼的功能是通過一個 WHILE 循環添加一個重試操作。我將重試次數設置爲 3,重試次數是可以配置的。至少我們現在有了一種在 T-SQL 內重試一個死鎖犧牲品代碼的方法 — 這是我們過去一直無法做到的。 
       但是,需要注意整個事務是在 WHILE 循環內進行的 — 而不是在循環外部。因此執行循環時,事務不僅在每個循環體內部開始,而且也在其中結束 — 不是 TRY 語句塊執行完畢,返回一個 COMMIT,就是 CATCH 語句塊執行,返回一個 ROLLBACK。如果 TRY 成功,TRY 語句塊將以一個 BREAK 語句結束,退出 WHILE 循環。否則,CATCH 語句塊將重試計數器加 1,以一個 CONTINUE 語句結束本次循環,重新執行下次 WHILE 循環。事實上,您有實現重試事務的代碼 — 就像錯誤 1205 告訴我們做的那樣。但現在,重試操作完全在 T-SQL 內部完成。 
       SQL Server 2005 也提供幫助解決死鎖問題的其他方法,例如 SNAPSHOT ISOLATION 級別和用於 READ COMMITTED 的新選項(稱爲 READ COMMITTED SNAPSHOT)。然而,這一事實 — 現在,通過 SQL Server 2005,您能夠對事務進行編碼並捕獲死鎖錯誤(並重試它們) — 已經意味着您擁有一個可任意支配、功能更加強大的工具。 
       在後一篇轉貼的文章中我們將對sql server2000中產生死鎖的問題作一個總結,來幫助我們如何在實際使用中儘量減少死鎖!
 
發佈了34 篇原創文章 · 獲贊 0 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章