SQL Server事務的隔離級別和鎖


背景

       當用戶並發嘗試訪問同一數據的時,SQL Server嘗試用鎖來隔離不一致的數據和使用隔離級別查詢數據時控制一致性(數據該如何讀取),說起鎖就會聯想到事務,事務是一個工作單元,包括查詢/更新數據和數據定義。

鎖類型

在SQL Server中,根據資源的不同,鎖分為以下三種類型:
    行鎖:是SQL Server中數據級別中粒度最小的鎖級別,行鎖根據表是否存在聚集索引,分為鍵值鎖和標識鎖
    頁鎖:針對某個數據頁添加的鎖,在T-SQL語句中,使用了頁鎖就不會在使用相同類型的行鎖,反之依然,在對數據頁加鎖后,無法在對其添加不兼容的鎖
    表鎖:添加表鎖則無法添加與其不兼容的頁å鎖和行鎖

鎖模式

   共享鎖(S):發生在數據查找之前,多個事務的共享鎖之間可以共存
   排他鎖(X):發生在數據更新之前,排他鎖是一個獨占鎖,與其他鎖都不兼容
   更新鎖(U):發生在更新語句中,更新鎖用來查找數據,當查找的數據不是要更新的數據時轉化為S鎖,當是要更新的數據時轉化為X鎖
   意向鎖:發生在較低粒度級別的資源獲取之前,表示對該資源下低粒度的資源添加對應的鎖,意向鎖有分為:意向共享鎖(IS) ,意向排他鎖(IX),意向更新鎖(IU),共享意向排他鎖(SIX),共享意向更新鎖(SIU),更新意向排他鎖(UIX)
   共享鎖/排他鎖/更新鎖一般作用在較低級別上,例如數據行或數據頁,意向鎖一般作用在較高的級別上,例如數據表或數據。鎖是有層級結構的,若在數據行上持有排他鎖的時候,則會在所在的數據頁上持有意向排他鎖. 在一個事務中,可能由於鎖持有的時間太長或個數太多,出於節約資源的考慮,會造成鎖升級
   除了上述的鎖之外,還有幾個特殊類型的鎖,例如架構鎖,架構鎖包含兩種模式,架構穩定鎖(Sch-S)和架構更新鎖(Sch-M) ,架構穩定鎖用來穩定架構,當查詢表數據的時候,會對表添加架構穩定鎖,防止架構發生改變。當執行DDL語句的時候,會使用架構更新鎖,確保沒有任何資源對表的占用。大數據量的表避免執行DDL操作,這樣會造成架構更新鎖長時間占用資源,影響其他操作,除非必要不然不要執行DDL語句,如在必要的情況下添加字段,需要先給字段初始化,在設置為非空。

鎖的兼容性


如何查看一個事務中所請求的鎖類型和鎖的順序,可使用SQL Profiler 查看 Mode 屬性

數據准備

IF OBJECT_ID('dbo.Nums','u') IS NOT NULL
    DROP TABLE dbo.Nums;
GO
CREATE TABLE dbo.Nums
(
     ID INT PRIMARY KEY,
     NUM INT
);
GO
IF EXISTS(SELECT * FROM SYS.SEQUENCES WHERE OBJECT_ID=OBJECT_ID('dbo.NumSequence'))
    DROP SEQUENCE dbo.NumSequence;
GO
CREATE SEQUENCE dbo.NumSequence
    MINVALUE 1
    MAXVALUE 1000
    NO CYCLE
GO
DECLARE @num AS INT = NEXT VALUE FOR dbo.NumSequence
INSERT INTO dbo.Nums VALUES(@num,@num);
GO 1

運行UPDATE dbo.Nums SET Num += 1
查看SQL Profiler 的跟蹤,可以清楚的看到鎖的請求順序和類型(請自定配置跟蹤模版,以便於想要看到自己想要的屬性)

事務的隔離級別

事務

事務是一個工作單元,包含查詢/修改數據以及修改數據定義的多個活動的組合,說起事務就需要提起事務的四個基本特性ACID:
   原子性:事務要么全部成功,要么全部失敗。
   一致性:事務為提交前或者事務失敗后,數據都和未開始事務之前一致
   隔離性:事務與事務之間互不干擾
   持久性:事務成功后會被永久保存起來,不會在被回滾

隔離級別

事務的隔離級別控制並發用戶的讀取和寫入的行為,即不同的隔離界別對鎖的控制方式不一樣,隔離級別主要分為兩種類型:悲觀並發控制和樂觀並發控制,悲觀並發控制有:READ UNCPOMMITTED / READ COMMITTED (會話默認) /REPEATABLE READ / SERIALIZABLE . 樂觀並發控制主要以在Tempdb中創建快照的方式來實現,有:SNAPSHOT 和 READ COMMITTED SHAPSHOT,也被稱為基於行版本的控制的隔離級別。

READ UNCOMMITTED

此隔離級別的主要特點是可以讀取其他事務中未提交更改的數據,該隔離級別下請求查詢的數據不需要共享鎖,這樣對於請求的行正在被更改,不會出現阻塞,這就造成了臟讀.此隔離級別是最低的隔離級別,並發性良好,但是對於數據的一致性方面有缺陷,在一些不重要的查詢中可以采用這種方式
以上面的表為例,開始兩個會話,在會話1中運行如下代碼:

BEGIN TRAN
	UPDATE  dbo.Nums SET NUM = 10
	WHERE ID = 1

開啟會話2並且運行如下代碼:

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
GO
SELECT * FROM dbo.Nums WHERE ID = 1

查看運行結果

在事務未提交成情況下,卻讀取到了數據,這就是臟讀,可以通過SQL Profiler 查看具體的請求鎖的類型和順序。


如圖可以看出,對於會話2只請求了架構穩定鎖(Sch-S) 並未請求共享鎖

READ COMMITTED

此隔離級別可以看作是對READ UMCOMMITTED 隔離級別的升級,解決帶了臟讀的問題,主要方式是對應查詢數據的請求需要先請求共享鎖定,由於鎖之間的兼容性,造成阻塞,但是該模式也會帶來一個問題那就是不可重復讀,在同一事務中的兩個相同的查詢 查出來的結果不一致,主要是因為該隔離級別對應共享鎖並不會一致保持,在兩條查詢語句之間是沒有鎖存在的,這樣其他事務就是更新數據
以上面的表為例,開始兩個會話,在會話1中運行如下代碼:

BEGIN TRAN
	UPDATE  dbo.Nums SET NUM = 10
	WHERE ID = 1 

在會話2中運行如下代碼,該會話會被阻塞

SET TRANSACTION ISOLATION LEVEL READ COMMITTED
GO
SELECT * FROM dbo.Nums WHERE ID = 1

打開會話3運行如下語句,查看當前阻塞狀態,連接信息,阻塞語句等其他信息

SELECT request_session_id,resource_type,resource_database_id,DB_NAME(resource_database_id) AS dbname,resource_associated_entity_id,request_mode,request_status FROM sys.dm_tran_locks

運行結果如圖:

從圖中可以看出當前,會話55的請求狀態為WAIT,也就是阻塞狀態,圖中54為UPDATE操作的DML正在持有一個更新鎖(X).進一步查看進程的相關信息,運行如下代碼

SELECT session_id,most_recent_session_id,connect_time,last_read,last_write, most_recent_sql_handle FROM sys.dm_exec_connections WHERE session_id IN (54,55)


可以看到各個進程的連接時間,最后一次讀取時間和最后一次寫入時間,和對應的T-SQL語句,要想查看具體的語句信息請運行如下代碼

SELECT session_id,text FROM sys.dm_exec_connections CROSS APPLY sys.dm_exec_sql_text(most_recent_sql_handle) AS A WHERE session_id IN (54,55)


可以具體的查看到執行語句,要想知道具體某個會話阻塞原因,即正在等待哪個會話的資源,運行如下語句

SELECT session_id,blocking_session_id,command,text,database_id,wait_type,wait_resource,wait_time FROM sys.dm_exec_requests cross apply sys.dm_exec_sql_text(sql_handle)
WHERE blocking_session_id > 0


從圖中可以看出,會話55正在等待會話54及競爭的資源信息,等待類型和等待時間,從上述的語句可以輕松查看想要知道的信息,對於各個會話對鎖的請求順序和類型請自行查看SQL Profiler.
下面我們來說說不可重復讀的問題,新建會話1運行如下代碼

SET TRANSACTION ISOLATION LEVEL READ COMMITTED
GO
BEGIN TRAN
    SELECT * FROM dbo.Nums WHERE ID=1
    WAITFOR DELAY '00:00:10'
    SELECT * FROM dbo.Nums WHERE ID=1

新建會話2並運行如下代碼

BEGIN TRAN
    UPDATE dbo.Nums SET NUM+=1 WHERE ID = 1  
    COMMIT TRAN

查看會話1的運行結果如圖,從圖中可以看出兩次讀取出來的數據不一致,這就是不可重復讀

REPEATABLE READ

此隔離級別可以看作的是READ COMMITTED 的升級,該模式可以解決READ COMMITTED 的不可重復讀的問題,主要是因為該級別下對共享鎖的占用時間較長,會一直持續到事務的結束。但是該模式也會存在一個叫做幻讀的缺陷,幻讀指的是在查找一定范圍內的數據時,其他事務對該范圍的數據進行INSERT操作,導致再次執行相同的查詢語句,查詢的結果可能多或者是和第一句不一致,造成幻讀的原因是因為被鎖定的數據行是在第一次查詢數據時確定的,對未來的數據並沒有鎖。此隔離級別不建議在更新頻率較高的環境下使用,會造成性能不佳
以上面的表為例,打開兩個會話,在會話1中運行下面的代碼:

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
GO
BEGIN TRAN
    SELECT * FROM dbo.Nums WHERE ID=1
    WAITFOR DELAY '00:00:10'
    SELECT * FROM dbo.Nums WHERE ID=1

打開會話2並且運行如下代碼

BEGIN TRAN
    UPDATE dbo.Nums SET NUM+=1 WHERE ID = 1  
COMMIT TRAN

查看結果:

運行過程中可以發現UPDATE的DML會一直等待會話1中事務的提交,並不會造成不可重復讀,下面來演示下幻讀的問題,重新打開兩個會話,在會話1中運行下面的代碼:

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
GO
BEGIN TRAN
    SELECT * FROM dbo.Nums 
    WAITFOR DELAY '00:00:10'
    SELECT * FROM dbo.Nums 
COMMIT TRAN

打開會話2運行如下代碼:

BEGIN TRAN
    INSERT INTO dbo.Nums VALUES(2,2)
COMMIT TRAN

運行結果:

會話2並沒有被阻塞,這次查看下會話1的運行結果可以看到,讀取出了2行數據,被稱為幻讀,關於鎖的請求類型和順序請打開SQL Profiler 自行查看.

SERIALIZABLE

此隔離級別可以看作是 REPEADTABLE READ 的升級,解決了幻讀的問題,因為該模式下不僅可以鎖定第一次查詢的數據行,還可以鎖定未來滿足條件的數據行,是一個區間鎖的概念,該級別不會出現上述的問題,但是相對的代價就是一致性強犧牲了並發性
以上表為例,修改會話1的隔離級別為 SERIALIZABLE,代碼如下:


SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

從結果可以看到會話2一直在等待會話1的完成,關於鎖的請求類型和順序請打開SQL Profiler 自行查看.

SNAPSHOT

當前隔離級別和接下來要介紹的隔離級別都是樂觀並發控制的兩種模式,又稱行版本控制的隔離級別,在tempdb中存儲事務未提交之前的數據行,使用基於行版本的控制隔離級別不會請求共享鎖,對於查詢數據的請求直接從快照讀取,但是這種快照方式還是很消耗性能的,尤其是對於更新或刪除操作,仍然會出現阻塞. SNAPSHOT級別對快照的讀取是以事務為單位的。同一個事務中的讀取操作都會讀取同一快照,無論其他事務是否更新了快照。在 READ COMMITTED 的隔離級別下還是會從快照讀取,但是其他模式就按照本身的控制方式進行控制,目標是源表,只有SNAPSHOT隔離級別可以檢測沖突。
要使用該隔離級別需要在數據庫中打開任意會話執行如下代碼:

ALTER DATABASE TEST  SET ALLOW_SNAPSHOT_ISOLATION ON

以上面的表為例,打開兩個會話,在會話1中運行如下代碼:

BEGIN TRAN
	UPDATE dbo.Nums set NUM +=1
	WHERE ID = 1

打開會話2並運行如下代碼:

SET TRANSACTION ISOLATION LEVEL SNAPSHOT
GO
BEGIN TRAN
	SELECT * FROM dbo.Nums	
	WHERE ID = 1

此時會話2並沒有被阻塞,而是返回了之前的版本,結果如下:

切換會會話1運行 COMMIT TRAN ,緊接着繼續在會話2中在執行一遍相同的查詢,執行結果如下

發現與上次的結果相同,但是會話1明明已經提交了,為什么還是原來的數據呢,這是因為該模式的特點,要是想讀取新的數據需要,需要提交本次事務,繼續在會話2中運行如下代碼:

COMMIT TRAN
BEGIN TRAN
	SELECT * FROM dbo.Nums	
	WHERE ID = 1
COMMIT TRAN

結果如圖所示:

下面看一個沖突檢測的例子
重新打開兩個會話,在會話1中運行如下代碼:

SET TRANSACTION ISOLATION LEVEL SNAPSHOT
GO
BEGIN TRAN
	SELECT * FROM dbo.Nums	
	WHERE ID = 1

打開會話2運行如下代碼:


BEGIN TRAN 
	UPDATE dbo.Nums SET NUM =10000
	WHERE ID =1

回到會話1,繼續運行如下代碼:

UPDATE dbo.Nums SET NUM =100
	WHERE ID =1

此時會話1出現阻塞,可以通過執行如下語句:

SELECT session_id,blocking_session_id,command,text,database_id,wait_type,wait_resource,wait_time FROM sys.dm_exec_requests cross apply sys.dm_exec_sql_text(sql_handle) WHERE blocking_session_id > 0

結果如圖所示:

從圖中可以看出競爭的資源是源表的數據行,並不是快照的,這就說明對於UPDATE 或者是DELETE 最終的目標是源表,切換會話2 運行 COMMIT TRAN 發現會話1中出現了錯誤:

READ COMMITTED SNAPSHOT 模式對於沖突檢測這一案例結果是不支持,會話1中的更新操作會成功,讀者可以自行實驗。

READ COMMITTED SNAPSHOT

同SNAPSHOT很像,但對於快照的讀取是以語句為單位的,同一個事務中的查詢數據的語句每次都讀取快照的最新版
要使用該隔離級別需要在數據庫中打開任意會話執行如下代碼:

ALTER DATABASE TEST SET READ_COMMITTED_SNAPSHOT ON

以上表為例,打開2個會話,在會話1運行如下代碼:

BEGIN TRAN 
	UPDATE dbo.Nums SET NUM +=1
	WHERE ID =1

打開會話2,並運行如下代碼:

BEGIN TRAN 
	SELECT * FROM dbo.Nums 
	WHERE ID =1

運行結果為:

是從快照中讀取出來的,繼續在會話1中運行 COMMIT TRAN ,之后在會話2中的當前事務中繼續執行相同的查詢,結果如下:

這就是之前所說的語句為單位的讀取快照,在這里有一個很有趣的現象就是,在會話2中並未設置隔離級別,這是因為默認情況下的隔離級別為 READ COMMITTED 由於運行了如上語句修改數據庫標記,故,會話的默認的隔離級別變成了 READ COMMITTED SNAPSHOT,當顯示修改為其他隔離級別是,則會按照修改后的隔離級別運行。若修改會話2的隔離級別為 READ UNCOMMITTED 時,並不會進行快照查詢,仍然出現了臟讀。

對於解決臟讀/不可重復讀/幻讀等問題,可以通過升級隔離級別的方式解決問題。

死鎖

說起鎖的問題,那當然少不了談起死鎖這種現象,主要發生於兩個或多個事務之間存在相互阻塞,造成死鎖,在SQL Server 中會犧牲工作最少的事務,SQL Server 可以設置一個DEADLOCK_PRIORITY的會話選項設置事務的在發生死鎖的情況下犧牲的順序,值在-10~10之間,在發生死鎖的情況下,會優先犧牲數值最低的事務,不管其做的工作有多么的重要,當存在平級的時候,將根據工作數量進行犧牲。
下面來演示一個死鎖的例子,以上面的表為例,並創建一個Nums副本表取名CopyNums,並添加(1,1)記錄,打開兩個會話,在會話1中執行如下代碼:

SET DEADLOCK_PRIORITY 0
BEGIN TRAN
	UPDATE dbo.Nums SET NUM=100
	WHERE ID = 1

打開會話2運行如下代碼:

SET DEADLOCK_PRIORITY 1
BEGIN TRAN
	UPDATE  dbo.CopyNums SET NUM = 100
	WHERE ID = 1

切換回會話1 繼續運行如下代碼:

SELECT * FROM dbo.CopyNums 
	WHERE ID = 1

此時會發生阻塞,等待排他鎖(X)釋放,切換會話2運行如下代碼:

SELECT * FROM dbo.Nums
	WHERE ID = 1

此次也會發生阻塞,但是阻塞一會你就會發現,會話1終止了,並出現如下錯誤:

為什么會終止的是會話1呢?可以發現在會話中我們設置了 DEADLOCK_PRIORITY,會犧牲數值低的那個會話事務,查看SQL Profiler 可以發現,確實有死鎖現象發生(為了清晰僅顯示死鎖)

那么既然死鎖會發生,就要有對應的避免死鎖的對策:
   1. 事務時間越長,保持鎖的時間就越長,造成死鎖的可能性就越大,檢查事務中是否放置了過多的不應該屬於同一工作單元的邏輯,有的話請移除到,從而縮短事務的時間
   2. 上述死鎖發生的關鍵在於訪問順序的問題,將兩個會話中的語句變成一個順序(都先操作Nums 或者 CopyNums ),就沒有了死鎖現象,所以在沒有邏輯的單元中,調換順序也會減少死鎖的發生
   3. 考慮選擇隔離級別,不同隔離級別對鎖的控制方式不一樣,例如:行版本控制就不會請求共享鎖(S)


免責聲明!

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



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