在SQL Server標准的已提交讀(READ COMMITTED)隔離級別下,一個讀操作會和一個寫操作相互阻塞。未提交讀(READ UNCOMMITTED)雖然不會有這種阻塞,但是讀操作可能會讀到臟數據,這是大部分用戶不能接受的。有些關系型數據庫(例如Oracle)使用的是另一種處理方式。在任何一個修改之前,先對修改前的版本做一個復制[WX1] ,后續的一切讀操作都會去讀這個復制的版本,修改將創建一個新的版本。在這種處理方式下,讀、寫操作不會相互阻塞。使用這種行版本控制機制的好處,是程序的並發性比較高,但是缺點是用戶讀到的雖然不是一個臟數據,但是可能是個正在被修改馬上就要過期的數據值[WX2] 。如果根據這個過期的值做數據修改,會產生邏輯錯誤。
[WX1]復制的內容保存在tempdb當中。
[WX2]假如讀跟寫同時進行,讀到的不是現在正被修改的值,如果是讀到正被修改的值那就是臟讀了。讀到的是修改前的值。但是這個值隨時會過期。等到修改完就過期了。
有些用戶可能為了更高的並發性而不在乎這種缺點,所以更喜歡Oracle的那種處理方法。為了滿足這部分用戶的需求,SQL Server 2005也引入了這種機制,來實現類似的功能。所以選取行版本控制隔離級別也可以成為消除阻塞和死鎖的一種手段。
SQL Server有兩種行版本控制,使用行版本控制的已提交讀隔離(READ_COMMITTED_SNAPSHOT)和直接使用SNAPSHOT事務隔離級別。
- READ_COMMITTED_SNAPSHOT數據庫選項為ON時,READ_COMMITTED事務通過使用行版本控制提供語句級讀取一致性。
- ALLOW_SNAPSHOT_ISOLATION數據庫選項為ON時,SNAPSHOT事務通過使用行版本控制提供事務級讀取一致性。
下列示例可以說明使用普通已提交讀事務,行版本控制的快照隔離事務和行版本控制的已提交讀事務的行為差異。
實驗1:Read Committed Isolation level
query1:事務1

USE AdventureWorks; GO --step1:開啟第一個事務 BEGIN TRAN tran1 --step2:執行select操作,查看VacationHours,對查找的記錄加S鎖 SELECT EmployeeID, VacationHours FROM HumanResources.Employee WHERE EmployeeID = 4; --step6: 在第一個事務中重新運行查詢語句,發現查詢被阻塞 --這是因為在VacationHours上面有排他鎖,現在要查詢VacationHours字段又必須獲得S鎖,但是X鎖與S鎖沖突 --所以不能執行查詢,被阻塞. SELECT EmployeeID, VacationHours FROM HumanResources.Employee WHERE EmployeeID = 4; --step8:因為會話2已經提交了事務,不再阻塞當前查詢,因此返回會話2修改好的新數據:40 --step9:回滾或者提交事務 ROLLBACK TRANSACTION; commit tran tran1 GO
query2:事務2

USE AdventureWorks; GO --step3:開啟第二個事務 BEGIN TRAN tran2; --step4:修改VacationHours,需要獲得更新鎖U,在VacationHours上有S鎖,US不沖突,因此可以進行修改. --在修改VacationHours以后,更新鎖U變成了排他鎖X UPDATE HumanResources.Employee SET VacationHours = VacationHours - 8 WHERE EmployeeID = 4; -- step5:在當前事務中查詢VacationHours,發現只有40小時 SELECT VacationHours FROM HumanResources.Employee WHERE EmployeeID = 4; --step7:回滾事務 rollback tran tran2 --commit tran tran2
總結:
- 事務1中的讀操作沒有阻塞事務2中的寫操作
- 事務2中的更新操作阻塞了事務1中后來的讀操作,如下圖所示:
- 事務1兩次查詢得到的數據分別是48跟40,兩次獲得的數據內容不一樣。所以也成read committed為不可重復讀。在read committed隔離級別中,只在語句級別加鎖,當語句執行完以后自動釋放鎖。比如事務1中的第一次查詢,雖然查詢在事務中進行,並且事務沒有提交,但是此時查詢語句執行完以后在table上就找不到鎖了。
實驗2:Snapshot Isolation
此示例中,在快照隔離下運行的事務將讀取數據,然后由另一事務修改此數據。快照事務不會被其他事務執行的更新操作所阻塞,它忽略數據的修改繼續從版本化的行讀取數據。也就是說,讀取到的是數據修改前的版本。但是,當快照事務嘗試修改已由其他事務修改的數據時,它將生成錯誤並終止。
query1:事務1,快照事務

--實驗2:Read Committed Snapshot Isolation level ------------------- USE AdventureWorks; GO --step1:啟用快照隔離 ALTER DATABASE AdventureWorks SET ALLOW_SNAPSHOT_ISOLATION ON; GO --step2:設置使用快照隔離級別,前面沒有設定是因為數據庫默認的隔離界別就是Read Committed SET TRANSACTION ISOLATION LEVEL SNAPSHOT; GO --step3:開啟第一個事務 BEGIN TRAN tran1 --step4:執行select操作,查看VacationHours,對查找的記錄加S鎖 SELECT EmployeeID, VacationHours FROM HumanResources.Employee WHERE EmployeeID = 4; --step8:在事務2中修改了數據以后,在事務1中再次運行查詢語句 --此時查詢語句沒有被阻塞,返回的值是48,也就是事務2修改之前的數據 --這是因為事務1是從版本化的行讀取數據 SELECT EmployeeID, VacationHours FROM HumanResources.Employee WHERE EmployeeID = 4; --step10:在事務2提交以后,事務1再次執行查詢操作 --發現查詢結果還是48,這是因為事務依然從版本化的行中讀取數據 SELECT EmployeeID, VacationHours FROM HumanResources.Employee WHERE EmployeeID = 4; --step11:在事務2提交修改以后,事務1想再做任何修改時,這里我們修改SickLeaveHours字段的值 --此時會遇到3960錯誤,事務1會自動回滾,事務2中的修改不會被回滾. UPDATE HumanResources.Employee SET SickLeaveHours = SickLeaveHours - 8 WHERE EmployeeID = 4; --附:假如事務2中執行了修改操作,但是沒有提交,此時在事務1中執行修改操作會被阻塞 --此時如果提交事務2中的修改操縱,事務1會遇到3960錯誤,跟上面一樣. UPDATE HumanResources.Employee SET SickLeaveHours = SickLeaveHours - 8 WHERE EmployeeID = 4; rollback tran tran1 commit tran tran1 /* Msg 3960, Level 16, State 2, Line 1 Snapshot isolation transaction aborted due to update conflict. You cannot use snapshot isolation to access table 'HumanResources.Employee' directly or indirectly in database 'AdventureWorks' to update, delete, or insert the row that has been modified or deleted by another transaction. Retry the transaction or change the isolation level for the update/delete statement. */ --實驗2結束------------------------------------
query2:事務2

--實驗2:Read Committed Snapshot Isolation level ------------------- USE AdventureWorks; GO --step5:開啟第二個事務 BEGIN TRAN tran2; --step6:修改VacationHours,需要獲得更新鎖U,在VacationHours上有S鎖,US不沖突,因此可以進行修改. --在修改VacationHours以后,更新鎖U變成了排他鎖X UPDATE HumanResources.Employee SET VacationHours = VacationHours - 8 WHERE EmployeeID = 4; -- step7:在當前事務中查詢VacationHours,發現只有40小時 SELECT VacationHours FROM HumanResources.Employee WHERE EmployeeID = 4; --step9:提交事務2 commit tran tran2 --實驗2結束------------------------------------
總結:
- 快照事務1的讀操作沒有阻塞普通事務2的讀操作,但是阻塞了事務2的刪除操作,如果在事務2中執行delete操作的話會報錯:Employees cannot be deleted. They can only be marked as not current.Msg 3609, Level 16, State 1, Line 1The transaction ended in the trigger. The batch has been aborted.
- 普通事務2的更新操作,沒有阻塞事務1的讀操作,但是我們發現事務1中讀到數據是事務2更新之前的內容。因為讀取的是版本化中的行數據。
在上述實驗中,我們發現下面兩條語句使一起使用的,也就是首先允許數據庫開啟snapshot isolation,然后再將isolation level設定為snapshot。
--step1:啟用快照隔離 ALTER DATABASE AdventureWorks SET ALLOW_SNAPSHOT_ISOLATION ON; GO --step2:設置使用快照隔離級別,前面沒有設定是因為數據庫默認的隔離界別就是Read Committed SET TRANSACTION ISOLATION LEVEL SNAPSHOT; GO
在執行完step1以前,我們可以在sys.databases中查看AdvantureWorks的snapshot_isolation_state和 is_read_committed_snapshot_on這兩個屬性:
SELECT name,snapshot_isolation_state, snapshot_isolation_state_desc, is_read_committed_snapshot_on FROM sys.databases where name='AdventureWorks';
查詢結果如下圖所示:
假如我們不執行step1,只執行step2,然后開啟事務進行查詢,會報如下錯誤:
Msg 3952, Level 16, State 1, Line 3
Snapshot isolation transaction failed accessing database 'AdventureWorks' because snapshot isolation is not allowed in this database. Use ALTER DATABASE to allow snapshot isolation.
執行完step1以后,我們再次查看sys.databases中的內容,發現snapshot_isolation_state由0變為1,如下圖所示:
實驗3:使用行版本控制的已提交讀隔離(READ_COMMITTED_SNAPSHOT)
在此示例中,使用行版本控制的已提交讀事務與其他事務並發運行。已提交讀事務的行為與快照事務的行為有所不同。與快照事務相同的是,即使其他事務修改了數據,已提交讀事務也將讀取版本化的行。與快照事務不同的是,已提交讀將執行下列操作:
query1:事務1

--實驗3:READ_COMMITTED_SNAPSHOT ------------------- --stpe1:啟用行版本控制的已提交讀 -- 注意運行這句話的時候,不可以有其他連接同時使用AdventureWorks use master ALTER DATABASE AdventureWorks SET READ_COMMITTED_SNAPSHOT ON; GO --step2:設置使用已提交讀隔離級別 USE AdventureWorks; GO SET TRANSACTION ISOLATION LEVEL READ COMMITTED; GO --step3:開啟第一個事務 BEGIN TRAN tran1 --step4:執行select操作,查看VacationHours,對查找的記錄加S鎖 SELECT EmployeeID, VacationHours FROM HumanResources.Employee WHERE EmployeeID = 4; --step8:在事務2中修改了數據以后,在事務1中再次運行查詢語句 --此時查詢語句沒有被阻塞,返回的值是48,也就是事務2修改之前的數據 --這是因為事務1是從版本化的行讀取數據 SELECT EmployeeID, VacationHours FROM HumanResources.Employee WHERE EmployeeID = 4; --step10:在事務2提交以后,事務1再次執行查詢操作 -- 這里和實驗2不同,事務1始終返回已提交的值,所以這里返回40,因為會話2已經提交了事務 SELECT EmployeeID, VacationHours FROM HumanResources.Employee WHERE EmployeeID = 4; --step11:這里修改會成功,不會報錯. UPDATE HumanResources.Employee SET SickLeaveHours = SickLeaveHours - 8 WHERE EmployeeID = 4; rollback tran tran1 --實驗3結束------------------------------------
query2:事務2

--實驗3:READ_COMMITTED_SNAPSHOT ------------------- USE AdventureWorks; GO --step5:開啟第二個事務 BEGIN TRAN tran2; --step6:修改VacationHours,需要獲得更新鎖U,在VacationHours上有S鎖,US不沖突,因此可以進行修改. --在修改VacationHours以后,更新鎖U變成了排他鎖X UPDATE HumanResources.Employee SET VacationHours = VacationHours - 8 WHERE EmployeeID = 4; -- step7:在當前事務中查詢VacationHours,發現只有40小時 SELECT VacationHours FROM HumanResources.Employee WHERE EmployeeID = 4; --step9:提交事務2 commit tran tran2 --實驗3結束------------------------------------
總結:
- 在事務2修改數據之后,提交之前,事務1中讀到的是快照數據,也就是事務2沒有修改之前的值。
- 在事務2提交修改之后,事務1讀到了修改之后的數據。並且事務1可以修改由其他數據修改了的數據。
結論
- 使用 sys.databases 目錄視圖可以確定兩個行版本控制數據庫選項的狀態。
- 對用戶表和存儲在 master 和 msdb 中的某些系統表的任何更新都會生成行版本。
- 在 master 和 msdb 數據庫中,ALLOW_SNAPSHOT_ISOLATION 選項自動設置為 ON,並且不能禁用。
- 在 master 數據庫、tempdb 數據庫或 msdb 數據庫中,用戶不能將 READ_COMMITTED_SNAPSHOT 選項設置為 ON。
- 從上面的測試可以看到,原先會發生阻塞的兩個會話在使用行版本控制的隔離級別后,都不會遇到阻塞了。但是兩種行版本控制的結果又有不同。可以用表1來總結。
表1 使用行版本控制隔離級別后的不同
會話1 |
會話2 |
結果 |
||||
A. 普通已提交事務 |
B. 使用快照隔離 |
C. 使用行版本控制的已提交讀 |
||||
BEGIN TRAN 查詢1 |
|
48 |
||||
|
BEGIN TRAN 修改1 |
成功 |
||||
|
查詢1 |
40 |
||||
查詢2 |
|
被阻塞 |
48 |
|||
|
COMMIT TRAN |
查詢2返回40 |
|
|||
查詢3 |
|
40 |
48 |
40 |
||
修改2 ROLLBACK TRAN |
|
成功 |
失敗 |
成功 |
補充:如果要查看鎖狀態,可以使用如下兩種方法:

--常看鎖狀態有一下兩種方式 --1.查看視圖sys.dm_tran_locks SELECT request_session_id, resource_type, resource_associated_entity_id, request_status, request_mode, resource_description FROM sys.dm_tran_locks --2.使用存儲過程 sp_lock