事務隔離級別神話與誤解


在今天的文章里我想談下SQL Server里現存的各種事務隔離級別的神話和誤解。主要我會談談下列話題:

  • 什么是事務隔離級別(Transaction Isolation Levels)?
  • NOLOCK從不阻塞!?
  • 提交讀(Read Committed)不會持鎖!?
  • Key Range Locks只針對可串行化?!

好,讓我們從第1個開始奠定SQL Server里事務隔離級別的基礎。

什么是事務隔離級別(Transaction Isolation Levels)?

每次當我站在客戶角度,處理各類亂七八糟的SQL Server問題(或進行SQL Server體檢),有時問題的根源是躲在數據庫里的鎖/阻塞行為。當你有一個糟糕的查詢(可能是丟失一個非常重要的索引),整個數據庫的性能都會下降。

你是否嘗試過啟動一個新的事務(卻不曾提交它),它會獲取你生產數據庫主數據表的排它鎖(exclusive lock)?相信我:你數據庫的性能和生產力就會掛掉——就在你面前!!!因此聽我老人言:不要這樣做!

當我們在任何關系數據庫里談論鎖和阻塞,我們也要談到那個數據庫管理系統(DBMS)所支持的各種事務隔離級別(這里我們指的是SQL Server)。SQL Server支持2中並發模式:“老”的悲觀並發控制,還有自SQL Server 2005起引入“新”的樂觀並發控制——一晃就是10年前的事了……

今天我想專心講下“老”的悲觀並發控制。在老的悲觀並發控制模式里,SQL Server支持4個不同的事務隔離級別:

  • 未提交讀/臟讀(Read Uncommitted)
  • 提交讀(Read committed)
  • 可重復讀(Repeatable Read)
  • 可串行化(Serializable)

我不想深入講解每個事務隔離級別,但我會給你每個隔離級別內部操作的概況,因為接下來的文章會用到那些信息。

當我們站在SQL Server的高度來看,你的事務包括讀取數據(SELECT查詢),還有修改數據(INSERT,UPDATE,DELETE,MERGE)。每次當你讀取數據時,SQL Server需要獲得共享鎖(Shared Locks (S))。每次當你修改數據時,SQL Server需要獲得排它鎖(Exclusive Locks (X))。這2個鎖是相互排斥的,就是說讀操作會阻塞寫操作,寫操作會阻塞讀操作。

現在使用事務隔離級別你就可以進行控制,讀操作可以持共享(S)鎖多少時間。寫操作總要獲得排它(X)鎖,你不能影響它。SQL Server默認使用的是提交讀(Read Committed)隔離級別,這意味着當你讀取一條記錄時,SQL Server需要獲得共享(S)鎖,當記錄讀取完成,共享(S)鎖就會釋放。當你逐行讀取時,共享(S)鎖也是逐行獲取與釋放。

當你不想讀取操作獲得共享(S)鎖(事實並不推薦這樣做),你可以使用提交讀/臟讀(Read Uncommitted)隔離級別。未提交讀/臟讀意味着你可以讀取臟數據——尚未提交的數據。這是賊快的(每人可以阻塞你),但是另一方面是非常危險的,因為這是未提交的數據。想下,如果未提交的事務在你讀到數據后又撤銷了:在你手里的數據在數據庫里邏輯上並不存在。現在你的手非常的臟。在我處理SQL Server問題里,我看到很多用戶使用未提交讀/臟讀或NOLOCK查詢提示來避免SQL Server里的阻塞。那不是首選處理阻塞的方法。接下來你會看到,即使NOLOCK也會阻塞……

在提交讀里你會有所謂的不可重復讀(Non Repeatable Reads),因為當你在你的事務里2次讀取數據時,其他人可以修改數據。如果你想避免不可重復讀,你可以使用可重復讀(Repeatable Read)隔離級別,在可重復讀里,SQL Server持有共享(S)鎖,直到你用COMMITROLLBACK來結束你的事務。這意味這沒有人可以修改你讀取的數據,對於你的事務,你是可以重復讀的。

到目前為止在每個我們討論的隔離級別里,你都會得到所謂的虛影記錄(Phantom Records)——在你的記錄集里可以出現又消失的記錄。如果你想避免這些虛影記錄,你必須使用可串行化(Serializable)隔離級別,最有限制的隔離級別。在可串行化里SQL Server使用所謂的Key Range Locking來消除虛影記錄:你鎖定整個范圍的數據,因此沒有其他並發的事務可以插入其它記錄來阻止虛影記錄。

從這個介紹可以看到,你的隔離級別越多限制,你數據庫的並發操作會更受影響。因此你要正確選擇對的隔離級別。一直使用提交讀沒有意義,一直使用可串行化也沒有意義。和往常一樣,依具體情況而定。

現在我已經奠定了SQL Server里事務隔離級別的基礎,現在我會給你展示3個不同的情況,和我上述介紹的情況不會符合。在一些特定情況下,SQL Server會通過對特定SQL語句在底層(Under the Hood)改變事務隔離級別來保證你事務的准確性。我們開始吧……

NOLOCK從不阻塞!?

在簡介里我已經描述了,對於特定的SQL語句,在數據讀取時,NOLOCK查詢提示會阻止需要的共享(S)鎖。這會讓你的SQL語句非常快,因為SQL查詢不會被任何其他事務阻塞。我把NOLOCK稱為SQL Server里的加速器。

但遺憾的是,當你使用並發的像ALTER TABLE的DDL語句(數據定義語言(Data Definition Language,DDL))。在我們理解這個行為前,我們需要詳細看下DDL語句,當我們執行簡單SELECT查詢時,在SQL Server內部發生了什么。

當你使用ALTER TABLE的DDL語句修改表時,SQL Server在那個表上獲得所謂架構修改鎖(Schema Modification Lock (Sch-M)) 。當你現在對同個表同時運行SELECT查詢時,SQL Server第1步需要編譯物理執行計划。在SQL Server編譯階段需要所謂的架構修改鎖(Schema Modification Lock (Sch-M))

這2個鎖(Sch-M和Sch-S)是彼此互斥的!這意味着即使NOLOCK語句也會阻塞,因為在第1步你需要編譯執行計划。因此在SQL Server知道物理執行計划前,你的NOLOCK語句會阻塞。當你在生產系統里升級你的數據庫架構時,你有考慮過這個行為么?好好考慮下……讓我們用一個簡單的例子演示下這個行為。在第1步我會創建一個新表並往里插入一些記錄: 

 1 -- Create a new test table
 2 CREATE TABLE TestTable
 3 (
 4     Column1 INT,
 5     Column2 INT,
 6     Column3 INT
 7 )
 8 GO
 9 
10 -- Insert some test data
11 DECLARE @i INT = 0
12 
13 WHILE (@i < 10000)
14 BEGIN
15     INSERT INTO TestTable VALUES (@i, @i + 1, @i + 2)
16     SET @i += 1
17 END
18 GO

 然后我們通過執行一個ALTER TABLE的DDL語句來開始一個新的事務,給我們的表增加一個新列:

1 -- Begin a new transaction and do some work
2 BEGIN TRANSACTION
3 
4 -- Add a new column
5 -- DDL statements require a Sch-M lock on the objects that are modified.
6 -- In this case, the table "TestTable" gets a Sch-M lock (Schema modification lock)!
7 ALTER TABLE TestTable ADD Column4 INT

從剛才的代碼可以看到,這個事務還在進行中,尚未提交。因此讓我們在SSMS里打開新的會話來執行下列代碼: 

1 -- The statement is now blocking, even with the NOLOCK query hint!
2 -- SQL Server has to compile the query, and requests a Sch-S lock (Schema Stability lock).
3 -- This lock is incompatible with the Sch-M lock!
4 SELECT * FROM TestTable WITH (NOLOCK)
5 GO

你會馬上看到,這個SQL語句並沒有返回一條記錄,因為它被其它活動的事務阻塞——即使使用NOLOCK查詢提示!對這2個會話,你可以通過查詢DMV sys.dm_tran_locks對這個阻塞情況進一步故障排除。你會看到Sch-M鎖阻塞了Sch-S鎖。
使用NOLOCK查詢提示或未提交讀/臟讀事務隔離級別不能保證你的SQL語句馬上執行。在過去里很多用戶都在與此特定問題斗爭。 

提交讀(Read Committed)不會持鎖!?

在簡介里你已經學到提交讀事務隔離級別,共享(S)鎖只在記錄讀取期間把持。這意味着只要記錄被讀取,那個鎖就會被立即釋放。因此當你從一個表讀取數據,針對當前被處理的記錄,在這個時間點只有共享(S)鎖。這個描述只在你的執行計划沒有阻塞運算符——例如排序(sort)運算符是對的。當你的執行計划有這樣的運算符,意味着SQL Server需要創建你數據的副本。

在數據副本完成后,原始的表/索引數據就不需要保留。但當你處理小量數據時,創建數據副本不會影響你的性能。假設當你有VARCHAR(MAX)列定義的表時。在那個情況下每行的那列可以保存最大2GB的數據。創建數據副本只會吹干你的內存和TempDb。

為了避免這個問題,SQL Server只持共享(S)鎖到你語句的結束。因此在此同時不存在有人改變數據的可能(共享(S)鎖阻塞排它(X)鎖),SQL Server只引用原始,穩定,未改變的數據。因此,你的事務運行起來像在可重讀隔離級別,這會傷及你數據庫的擴展性。讓我們創建下列數據庫架構來演示這個行為: 

 1 -- Create a new table
 2 CREATE TABLE TestTable
 3 (
 4     ID INT IDENTITY(1, 1) NOT NULL PRIMARY KEY,
 5     Col2 INT,
 6     Col3 VARCHAR(MAX)
 7 )
 8 GO
 9 
10 -- Insert some records into the table
11 INSERT INTO TestTable VALUES (1, 'abc'), (2, 'def'), (3, 'ghi')
12 GO
13 
14 -- Begin a new transaction, so that we are blocking some records in the table
15 BEGIN TRANSACTION
16 
17 UPDATE TestTable SET Col2 = 1 WHERE ID = 3

 從代碼里你可以看到,我創建一個聚集表,有3條聚集鍵值為1,2,3的記錄。下一步我會開始一個新的事務,我們在聚集索引里鎖定第3條記錄。現在我們搭建好演示。在獨立的會話里,我們嘗試從表讀取記錄——顯然這個SELECT語句會阻塞:

1 -- This statement only acquires a key lock on the current record
2 SELECT Col3 FROM TestTable
3 GO

當我們從DMV sys.dm_tran_locks里查看,你會清楚看到那個SELECT語句在等待在聚集鍵值為3的第3個鎖。這是提交讀隔離級別的典型行為,因為在我們的執行計划里沒有阻塞運算符。但只要我們引入一個阻塞運算符(並讀取一個LOB數據類型),事情就會發生改變:

1 -- This statement only acquires a key lock on the current record
2 SELECT Col3 FROM TestTable
3 ORDER BY Col2
4 GO

如你所見,現在我們使用了ORDER BY字句,在執行計划里會給我們排序(sort)運算符。當然,這個SELECT語句會再次阻塞。但當我們查看DMV sys.dm_tran_locks時,你會看到提交讀隔離級別完全不同的行為:SQL Server現在在前2行(聚集鍵值為1,2)獲得共享(S)鎖,但不再釋放它們了!SQLServer把持這些鎖直到我們的SELECT語句完成,這是為了阻止對潛在數據的並發改變。

在可重讀隔離級別里,我們的SELECT語句會高效運行。當你設計你的下個表架構時,考慮下這點,還有在你主要事務表里包含LOB數據類型時也是。  

Key Range Locks只針對序列化?!

前段時間我碰到一個問題(在數據庫體檢期間),在提交讀事務這個默認隔離級別里碰到Key Range Locks。從這個文章的開始,我們就知道在可串行化隔離級別才會用到Key Range Locks。因此問題是這些Key Range Locks從哪里來的。

當我們進一步分析數據架構時,我們發現表用了外鍵約束,在那里級聯刪除(Cascading Deletes)被啟用。只要你對外鍵啟用級聯刪除,當你從父表里刪除記錄時,SQL Server就會在子表上使用Key Range Locks。這是對的,因為在級聯刪除期間,Key Range Locks可以阻止新記錄的插入。因此表的引用完整性被保持。我們來看一個具體的例子。在第1步我們創建2個表,在2個表之間定義外鍵,並啟用級聯刪除。 

 1 -- Create a new parent table
 2 CREATE TABLE Parent
 3 (
 4     Parent1 INT PRIMARY KEY NOT NULL,
 5     Parent2 INT NOT NULL
 6 )
 7 GO
 8 
 9 -- Create a new child table
10 CREATE TABLE Child
11 (
12     Child1 INT PRIMARY KEY NOT NULL,
13     Child2 INT NOT NULL,
14     -- The following column will contain a Foreign Key constraint
15     Parent1 INT NOT NULL
16 )
17 GO
18 
19 -- Create a foreign key constraint between both tables,
20 -- and enable Cascading Deletes on it
21 ALTER TABLE Child
22 ADD CONSTRAINT FK_Child_Parent
23 FOREIGN KEY (Parent1) REFERENCES Parent(Parent1)
24 ON DELETE CASCADE
25 GO
26 
27 -- Insert some test data
28 INSERT INTO Parent VALUES (1, 1), (2, 2), (3, 3)
29 INSERT INTO Child VALUES (1, 1, 1), (2, 2, 1), (3, 3, 1)
30 GO

 現在當你開始新的事務,你從父表里刪除值為1的記錄,SQL Server也會從子表里刪除對應的記錄(在這里是3條),因為我們啟用了級聯刪除:

 1 -- Start a new transaction and analyze the acquired locks
 2 BEGIN TRANSACTION
 3 
 4 -- This statement deletes the record from the parent table,
 5 -- and the 3 records from the child table
 6 DELETE FROM Parent
 7 WHERE Parent1 = 1
 8 
 9 -- SQL Server uses 3 RangeX-X locks, even with the default
10 -- Isolation Level of Read Committed
11 SELECT * FROM sys.dm_tran_locks
12 WHERE request_session_id = @@SPID
13 
14 COMMIT
15 GO

在事務執行期間,你可以查看下DMV sys.dm_tran_locks,你可以看到你的會話需要獲取3個RangeX-X鎖——Key Range Locks!在刪除期間,SQL Server需要這些鎖來阻止新記錄的插入。我們在這里可以看到,SQL Server明顯把你的事務隔離級別提升到可串行化來保證你事務的准確性。

小結

從這個文章可以看出,在SQL Server是沒有任何保證的。我經常問用戶,在SQL Server里,你們知道各個隔離級別,鎖行為的具體信息么。如果知道的話,我們就可以一起面對這篇文章里提到的不同現象。

從這里得出的最重要的結果是,對於提供的SQL語句,SQL Server是能提升隔離級別的。因此不要以為SQL Server總是在你設置的隔離級別里運行你的查詢。

感謝關注!

參考文章:

https://www.sqlpassion.at/archive/2014/01/21/myths-and-misconceptions-about-transaction-isolation-levels/


免責聲明!

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



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