本文並不全是原創,我參考的原文在這裏html
背景:sql
當用戶併發嘗試訪問同一數據的時,SQL Server嘗試用鎖來隔離不一致的數據和使用隔離級別查詢數據時控制一致性(數據該如何讀取),提及鎖就會聯想到事務,事務是一個工做單元,包括查詢/更新數據和數據定義。數據庫
在SQL Server中,根據資源的不一樣,鎖分爲如下三種類型:
行鎖:是SQL Server中數據級別中粒度最小的鎖級別,行鎖根據表是否存在彙集索引,分爲鍵值鎖和標識鎖
頁鎖:針對某個數據頁添加的鎖,在T-SQL語句中,使用了頁鎖就不會在使用相同類型的行鎖,反之依然,在對數據頁加鎖後,沒法在對其添加不兼容的鎖
表鎖:添加表鎖則沒法添加與其不兼容的頁å鎖和行鎖session
共享鎖(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
事務是一個工做單元,包含查詢/修改數據以及修改數據定義的多個活動的組合,提及事務就須要提起事務的四個基本特性ACID:
原子性:事務要麼所有成功,要麼所有失敗。
一致性:事務爲提交前或者事務失敗後,數據都和未開始事務以前一致
隔離性:事務與事務之間互不干擾
持久性:事務成功後會被永久保存起來,不會在被回滾app
事務的隔離級別控制併發用戶的讀取和寫入的行爲,即不一樣的隔離界別對鎖的控制方式不同,隔離級別主要分爲兩種類型:悲觀併發控制和樂觀併發控制,悲觀併發控制有:READ UNCPOMMITTED / READ COMMITTED (會話默認) /REPEATABLE READ / SERIALIZABLE . 樂觀併發控制主要以在Tempdb中建立快照的方式來實現,有:SNAPSHOT 和 READ COMMITTED SHAPSHOT,也被稱爲基於行版本的控制的隔離級別。性能
這個就至關於在查詢語句的後面加上with(nolock),此隔離級別的主要特色是能夠讀取其餘事務中未提交更改的數據(該語句 READ UNCOMMITTED是對整個批處理或SP不加鎖,所以 後面全部的T-SQL查詢語句都是不加S鎖的),該隔離級別下請求查詢的數據不須要共享鎖(例如 tranA是一個delete TestTable操做,同時你查詢TestTable的時候就須要先在TestTable的查詢行上面加S鎖,所以就要等待delete的操做結束,X鎖釋放掉,可是你使用 READ UNCOMMITTED以後,你在查詢的時候就不加S鎖了,因此就能訪問放了X鎖的語句),這樣對於請求的行正在被更改,不會出現阻塞,這就形成了髒讀.此隔離級別是最低的隔離級別,併發性良好,可是對於數據的一致性方面有缺陷,在一些不重要的查詢中能夠採用這種方式
以上面的表爲例,開始兩個會話,在會話1中運行以下代碼:大數據
BEGIN TRAN UPDATE dbo.Nums SET NUM = 10 WHERE ID = 1
開啓會話2而且運行以下代碼:spa
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED GO SELECT * FROM dbo.Nums WHERE ID = 1
查看運行結果
在事務未提交成狀況下,卻讀取到了數據,這就是髒讀。
另外一種情形:
--會話1 BEGIN TRAN UPDATE dbo.Nums SET NUM+=1 WHERE ID = 1 --會話2 SELECT * FROM dbo.Nums WHERE ID=1
你會發現會話2一直在等待,由於會話1在 ID = 1 的語句上面放了X鎖,而且處於事務中,而且事務沒有commit致使 ID = 1 的行一直被X鎖鎖住,而致使會話2的S鎖沒辦法放到ID = 1 的行上,因此一直在等待會話1事務的提交。,做爲對比,看下面的語句:
--會話1 BEGIN TRAN SELECT * FROM dbo.Nums WHERE ID=1 --會話2 UPDATE dbo.Nums SET NUM+=1 WHERE ID = 1
你會發現 會話2能夠執行成功,疑問:會話1在ID=1的行上面放了S鎖,而且會話1的事務也沒有結束,會話2要更新就必須放X鎖在ID=1的行上面,可是X鎖是互斥鎖,不可能放的上去啊?其實 讀取操做一完成,資源上的共享鎖(S 鎖)就會被當即釋放(除非將事務隔離級別設置爲可重複讀或更高級別,或者在事務持續時間內用鎖定提示保留共享鎖(S 鎖),而X鎖則是要等到事務提交以後纔會釋放),即便該讀取操做是在一個事務中,而且該事務未完成。
這個是系統默認的一種狀況。一樣的,這個語句做用的是整個批處理,即下面全部的TSQL語句都具有這一點。此隔離級別能夠看做是對READ UMCOMMITTED 隔離級別的升級,解決帶了髒讀的問題,主要由於當你查詢的時候須要先請求共享鎖定,因爲鎖之間的兼容性,形成阻塞(當你查詢的語句上面放了X鎖的時候),可是該模式也會帶來一個問題那就是不可重複讀(不理解看下面的例子),在同一事務中的兩個相同的查詢 查出來的結果不一致,主要是由於該隔離級別對應共享鎖並不會一直保持(也就是說兩條連續的查詢語句,每一條都會加鎖,可是兩條之間是沒有鎖的,所以update和delete語句會在兩條之間執行,致使兩條查詢語句顯示的結果不同),在兩條查詢語句之間是沒有鎖存在的,這樣其餘事務就是更新數據
以上面的表爲例,開始兩個會話,在會話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
根據上面獲得的session_id ,能夠用下面的語句查詢更具體的信息:
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)
能夠具體的查看到執行語句,要想知道具體某個會話阻塞緣由,即正在等待哪一個會話的資源,運行以下語句
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
會話 session_id 正在等待會話 blocking_session_id
下面咱們來講說不可重複讀的問題,新建會話1運行以下代碼
SET TRANSACTION ISOLATION LEVEL READ COMMITTED GO BEGIN TRAN SELECT * FROM dbo.Nums WHERE ID=1 --A語句 WAITFOR DELAY '00:00:10' SELECT * FROM dbo.Nums WHERE ID=1 --B語句
新建會話2並運行以下代碼
BEGIN TRAN UPDATE dbo.Nums SET NUM+=1 WHERE ID = 1 COMMIT TRAN
查看會話1的運行結果如圖,從圖中能夠看出兩次讀取出來的數據不一致,這就是不可重複讀,意思就是:會話1的A語句可以正常執行獲得結果集,而後釋放S鎖,而後會話2就開始了,因爲ID = 1
此隔離級別能夠看做的是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 COMMIT TRAN
打開會話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延時的10s內增長了一筆(不會被阻塞,由於insert不會放鎖在表上面),致使兩個相同的查詢結果卻不一致,這就是幻讀,固然,這也是
REPEATABLE READ和READ UNCOMMITTED共同的缺點。
此隔離級別能夠看做是 REPEADTABLE READ 的升級,解決了幻讀的問題,由於該模式下不只能夠鎖定第一次查詢的數據行,還能夠鎖定將來知足條件的數據行,是一個區間鎖的概念,該級別不會出現上述的問題,可是相對的代價就是一致性強犧牲了併發性
以上表爲例,修改會話1的隔離級別爲 SERIALIZABLE,代碼以下:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
從結果能夠看到會話2一直在等待會話1的完成,關於鎖的請求類型和順序請打開SQL Profiler 自行查看.
分看兩種狀況,
第一種:查詢中指定了主鍵id,那麼就只是鎖定了鍵值,會話2不須要等待,能夠直接執行
--會話1 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE BEGIN TRAN SELECT * FROM dbo.Nums where id=2 WAITFOR DELAY '00:00:10' SELECT * FROM dbo.Nums where id=2 COMMIT TRAN
--會話2
INSERT INTO dbo.Nums VALUES(14,5)
查詢的條件不是主鍵,那麼鎖定的就是整個表:
--會話1 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE BEGIN TRAN SELECT * FROM dbo.Nums where num=2 WAITFOR DELAY '00:00:10' SELECT * FROM dbo.Nums where num=2 COMMIT TRAN
--會話2 INSERT INTO dbo.Nums VALUES(12,3)
事實是會話2一直在等待會話1執行完以後才執行,可是會話2插入的也不在會話1的區域內
固然,若是會話2改爲 delete和update語句狀況和insert也是同樣的
當前隔離級別和接下來要介紹的隔離級別都是樂觀併發控制的兩種模式,又稱行版本控制的隔離級別,在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中的更新操做會成功,讀者能夠自行實驗。
同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)