SQL Server事務的隔離級別和鎖

背景

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

鎖類型

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

鎖模式

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

鎖的兼容性


如何查看一個事務中所請求的鎖類型和鎖的順序,可以使用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:
   原子性:事務要麼所有成功,要麼所有失敗。
   一致性:事務爲提交前或者事務失敗後,數據都和未開始事務以前一致
   隔離性:事務與事務之間互不干擾
   持久性:事務成功後會被永久保存起來,不會在被回滾app

隔離級別

事務的隔離級別控制併發用戶的讀取和寫入的行爲,即不一樣的隔離界別對鎖的控制方式不同,隔離級別主要分爲兩種類型:悲觀併發控制和樂觀併發控制,悲觀併發控制有: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) 並未請求共享鎖code

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)

相關文章
相關標籤/搜索