1. 程式人生 > >Microsoft SQL Server中的事務與併發詳解

Microsoft SQL Server中的事務與併發詳解

本篇索引:

1、事務

2、鎖定和阻塞

3、隔離級別

4、死鎖

一、事務

1.1 事務的概念

  事務是作為單個工作單元而執行的一系列操作,比如查詢和修改資料等。

  事務是資料庫併發控制的基本單位,一條或者一組語句要麼全部成功,對資料庫中的某些資料成功修改; 要麼全部不成功,資料庫中的資料還原到這些語句執行之前的樣子。

比如網上訂火車票,要麼你定票成功,餘票顯示就減一張; 要麼你定票失敗獲取取消訂票,餘票的數量還是那麼多。不允許出現你訂票成功了,餘票沒有減少或者你取消訂票了,餘票顯示卻少了一張的這種情況。這種不被允許出現的情況就要求購票和餘票減少這兩個不同的操作必須放在一起,成為一個完整的邏輯鏈,這樣就構成了一個事務。

1.2 事務的ACID特性

  原子性(Atomicity):事務的原子性是指一個事務中包含的一條語句或者多條語句構成了一個完整的邏輯單元,這個邏輯單元具有不可再分的原子性。這個邏輯單元要麼一起提交執行全部成功,要麼一起提交執行全部失敗。

  一致性(Consistency):可以理解為資料的完整性,事務的提交要確保在資料庫上的操作沒有破壞資料的完整性,比如說不要違背一些約束的資料插入或者修改行為。一旦破壞了資料的完整性,SQL Server 會回滾這個事務來確保資料庫中的資料是一致的。

  隔離性(Isolation):與資料庫中的事務隔離級別以及鎖相關,多個使用者可以對同一資料併發訪問而又不破壞資料的正確性和完整性。但是,並行事務的修改必須與其它並行事務的修改相互獨立,隔離。 但是在不同的隔離級別下,事務的讀取操作可能得到的結果是不同的。

  永續性(Durability):資料持久化,事務一旦對資料的操作完成並提交後,資料修改就已經完成,即使服務重啟這些資料也不會改變。相反,如果在事務的執行過程中,系統服務崩潰或者重啟,那麼事務所有的操作就會被回滾,即回到事務操作之前的狀態。

在極端斷電或者系統崩潰的情況下,一個發生在事務未提交之前,資料庫應該記錄了這個事務的"ID"和部分已經在資料庫上更新的資料。供電恢復資料庫重新啟動之後,這時完成全部撤銷和回滾操作。如果在事務提交之後的斷電,有可能更改的結果沒有正常寫入磁碟持久化,但是有可能丟失的資料會通過事務日誌自動恢復並重新生成以寫入磁碟完成持久化。

1.3 如何定義事務 

  (1)顯示定義:以BEGIN TRAN開始,提交的話則COMMIT提交事務,否則以ROLLBACK回滾事務。

--定義事務
BEGIN TRAN;
  INSERT INTO dbo.T1(keycol, col1, col2) VALUES(4,101,'C');
  INSERT INTO dbo.T1(keycol, col1, col2) VALUES(4,201,'X');
COMMIT TRAN;

  (2)隱式定義:SQL Server中預設把每個單獨的語句作為一個事務。

  換句話說,SQL Server預設在執行完每個語句之後就自動提交事務。當然,我們可以通過IMPLICIT_TRANSACTIONS會話選項來改變SQL Server處理預設事務的方式,該選項預設情況下是OFF。如果將其設定為ON,那麼就不必用BEGIN TRAN語句來表明事務開始,但仍然需要以COMMIT或ROLLBACK來標明事務完成。 

二、鎖定和阻塞

2.1 鎖

  (1)鎖是什麼鬼?

  鎖是事務獲取的一種控制資源,用於保護資料資源,防止其他事務對資料進行衝突的或不相容的訪問。

  (2)鎖模式及其相容性

  主要有兩種主要的鎖模式—排它鎖Exclusive Lock) 和 共享鎖Shared Lock)。

  當試圖修改資料時,事務會為所依賴的資料資源請求排它鎖,一旦授予,事務將一直持有排它鎖,直至事務完成。在事務執行過程中,其他事務就不能再獲得該資源的任何型別的鎖。

  當試圖讀取資料時,事務預設會為所依賴的資料資源請求共享鎖,讀操作一完成,就立即釋放共享鎖。在事務執行過程中,其他事務仍然能夠獲得該資源的共享鎖。

排它鎖和共享鎖的相容性
請求模式 已經授予排它鎖(X)   已經授予共享鎖(S)
授予請求的排它鎖?  否  否
授予請求的共享鎖?   否  是 

  (3)可鎖定資源的型別

  SQL Server可以鎖定不同型別或粒度的資源,這些資源型別包括RID或KEY(行),PAGE(頁)、物件(例如:表)及資料庫等。

2.2 阻塞

  (1)阻塞是個什麼鬼?

  如果一個事務持有某一資料資源上的鎖,而另一事務請求相同資源上的不相容的鎖,則對新鎖的請求將被阻塞,發出請求的事務進入等待狀態。預設情況下,被阻塞的請求會一直等待,直到原來的事務釋放相關的鎖。

只要能夠在合理的時間範圍內滿足請求,系統中的阻塞就是正常的。但是,如果一些請求等待了太長時間,可能就需要手工排除阻塞狀態,看看能採取什麼措施來防止這樣長時間的延遲。  

  (2)近距離觀測阻塞

  Step1.開啟兩個獨立的查詢視窗,這裡稱之為Connection A,Connection B

  Step2.在Connection A中執行以下程式碼(這裡productid=2的unitprice本來為19)

BEGIN TRAN;
  UPDATE Production.Products SET unitprice=unitprice+1.00
  WHERE productid=2;

  為了更新這一行,會話必須先獲得一個排它鎖,如果更新成功,SQL Server會向會話授予這個鎖。

  Step3.在Connection B中執行以下程式碼

SELECT productid, unitprice
FROM Production.Products
WHERE productid=2;

  預設情況下,該會話需要一個共享鎖,但因為共享鎖和排它鎖是不相容的,所以該會話被阻塞,進入等待狀態。

  

  (3)如何檢測阻塞

  假設我們的系統裡邊出現了阻塞,而且被阻塞了很長時間,如何去檢測和排除呢?

  ① 繼續上例,開啟一個新的會話,稱之為Connection C,查詢動態管理檢視(DMV)sys.dm_tran_locks:

-- Lock info
SELECT -- use * to explore
  request_session_id            AS spid,
  resource_type                 AS restype,
  resource_database_id          AS dbid,
  DB_NAME(resource_database_id) AS dbname,
  resource_description          AS res,
  resource_associated_entity_id AS resid,
  request_mode                  AS mode,
  request_status                AS status
FROM sys.dm_tran_locks;

  ② 執行上面的程式碼,可以得到以下輸出:

  

  ③ 每個會話都有唯一的伺服器程序識別符號(SPID),可以通過查詢@@SPID函式來檢視會話ID。另外,當前會話的SPID還可以在查詢視窗的標題欄中找到。

     

  ④ 在前面查詢的輸出中,可以觀察到程序53正在等待請求TSQLFundamental2008資料庫中一個行的共享鎖。但是,程序52持有同一個行上的排它鎖。沿著52和53的所層次結構向上檢查:(查詢sys.dm_exec_connections的動態管理檢視,篩選阻塞鏈中涉及到的那些SPID)

-- Connection info
SELECT -- use * to explore
  session_id AS spid,
  connect_time,
  last_read,
  last_write,
  most_recent_sql_handle
FROM sys.dm_exec_connections
WHERE session_id IN(52, 53);

  查詢結果輸出如下:

  

  ⑤ 藉助交叉聯接,和sys.dm_exec_sql_text表函式生成查詢結果:

-- SQL text
SELECT session_id, text 
FROM sys.dm_exec_connections
  CROSS APPLY sys.dm_exec_sql_text(most_recent_sql_handle) AS ST 
WHERE session_id IN(52, 53);

  查詢結果如下,我們可以達到阻塞鏈中涉及到的每個聯接最後呼叫的批處理程式碼:

  

  以上就顯示了程序53正在等待的執行程式碼,因為這是該程序最後執行的一個操作。對於阻塞程序來說,通過這個例子能夠看到是哪條語句導致了問題。

  (4)如何解除阻塞

  ① 設定超時時間

  首先取消掉原來Connection B中的查詢,然後執行以下程式碼:這裡我們限制會話等待釋放鎖的時間為5秒

-- Session B
SET LOCK_TIMEOUT 5000;

SELECT productid, unitprice
FROM Production.Products
WHERE productid=2;

  然後5秒之後我們可以看到以下執行結果:

  

  注意:鎖定超時不會引發事務回滾。

  ② KILL掉引起阻塞的程序

  在Connection C中執行以下語句,終止SPID=52中的更新事務而產生的效果,於是SPID=52中的事務的回滾,同時釋放排它鎖。

--KILL SPID=52
KILL 52;

  這時再在Connection B中執行查詢,便可以查到回滾後的結果(仍然是19):

  

三、隔離級別

  隔離級別用於決定如何控制併發使用者讀寫資料的操作。前面說到,讀操作預設使用共享鎖,寫操作需要使用排它鎖。對於操作獲得的鎖,以及鎖的持續時間來說,雖然不能控制寫操作的處理方式,但可以控制讀操作的處理方式。作為對讀操作的行為進行控制的一種結果,也會隱含地影響寫操作的行為方式。

  為此,可以在會話級別上用會話選項來設定隔離級別,也可以在查詢級別上用表提示(Table Hint)來設定隔離級別。

  在SQL Server中,可以設定的隔離級別有6個:READ UNCOMMITED(未提交讀)、READ COMMITED(已提交讀)、REPEATABLE READ(可重複讀)、SERIALIZEABLE(可序列化)、SNAPSHOT(快照)和READ COMMITED SNAPSHOT(已經提交讀隔離)。最後兩個SNAPSHOT和READ COMMITED SNAPSHOT是在SQL Server 2005中引入的。

  要設定整個會話級別的隔離級別,可以使用以下語句:

SET TRANSACTION ISOLATION LEVEL <isolation name>;

  也可以使用表提示來設定查詢級別的隔離級別:

SELECT ... FROM <table> WITH <isolation name>;

3.1 READ UNCOMMITED 未提交讀

  未提交讀是最低的隔離級別,讀操作不會請求共享鎖。換句話說,在該級別下的讀操作正在讀取資料時,寫操作可以同時對這些資料進行修改。

  同樣,使用兩個會話來模擬:

  Step1.在Connection A中執行以下程式碼,更新產品2的單價,為當前值(19.00)增加1.00,然後查詢該產品:

-- Connection A
BEGIN TRAN;

UPDATE Production.Products
SET unitprice = unitprice + 1.00
WHERE productid = 2;

SELECT productid, unitprice
FROM Production.Products
WHERE productid = 2;

  

  Step2.在Connection B中執行以下程式碼,首先設定隔離級別為未提交讀,再查詢產品2所在的記錄:

-- Connection B
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

SELECT productid, unitprice
FROM Production.Products
WHERE productid = 2;

  因為這個讀操作不用請求共享鎖,因此不會和其他事務發生衝突,該查詢返回瞭如下圖所示的修改後的狀態,即使這一狀態還沒有被提交:

  

  Step3.在Connection A中執行以下程式碼回滾事務:

ROLLBACK TRAN;

這個回滾操作撤銷了對產品2的更新,這時它的價格被修改回了19.00,但是讀操作此前獲得的20.00再也不會被提交了。這就是髒讀的一個例項!

  

3.2 READ COMMITED 已提交讀

  剛剛說到,未提交到會引起髒讀,能夠防止髒讀的最低隔離級別是已提交讀,這也是所有SQL Server版本預設使用的隔離級別。如其名稱所示,這個隔離級別只允許讀取已經提交的修改,它要求讀操作必須獲得共享鎖才能操作,從而防止讀取未提交的修改。

  繼續使用兩個會話來模擬:

  Step1.在Connection A中執行以下程式碼,更新產品2的價格,再查詢顯示價格:

BEGIN TRAN;

UPDATE Production.Products
SET unitprice = unitprice + 1.00
WHERE productid = 2;

SELECT productid, unitprice
FROM Production.Products
WHERE productid = 2;

  

  Step2.再在Connection B中執行以下程式碼,這段程式碼將會話的隔離級別設定為已提交讀,再查詢產品2所在的行記錄:

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

SELECT productid, unitprice
FROM Production.Products
WHERE productid = 2;

  這時該會話語句會被阻塞,因為它需要獲取共享鎖才能進行讀操作,而它與會話A的寫操作持有的排它鎖相沖突。這裡因為我設定了預設會話阻塞超時時間,所以出現了以下輸出:

  

  Step3.在Connection A中執行以下程式碼,提交事務:

COMMIT TRAN;

  Step4.回到Connection B,此時會得到以下輸出:

  

在已提交讀級別下,不會讀取髒資料,只能讀取已經提交過的修改。但是,該級別下,其他事務可以在兩個讀操作之間更改資料資源,讀操作因而可能每次得到不同的取值。這種現象被稱為 不可重複讀。  

3.3 REPEATABLE READ 可重複讀

  如果想保證在事務內進行的兩個讀操作之間,其他任何事務都不能修改由當前事務讀取的資料,則需要將隔離級別升級為可重複讀。在該級別下,十五中的讀操作不但需要獲得共享鎖才能讀資料,而且獲得的共享鎖將一直保持到事務完成為止。換句話說,在事務完成之前,沒有其他事務能夠獲得排它鎖以修改這一資料資源,由此來保證實現可重複的讀取。

  Step1.為了重新演示可重複讀的示例,首先需要將剛剛的測試資料清理掉,在Connection A和B中執行以下程式碼:

-- Clear Test Data
UPDATE Production.Products
SET unitprice = 19.00
WHERE productid = 2;
View Code

  Step2.在Connection A中執行以下程式碼,將會話的隔離級別設定為可重複讀,再查詢產品2所在的行記錄:

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

BEGIN TRAN;

  SELECT productid, unitprice
  FROM Production.Products
  WHERE productid = 2;

  

  這時該會話仍然持有產品2上的共享鎖,因為在該隔離級別下,共享鎖要一直保持到事務結束為止。

  Step3.在Connection B中嘗試對產品2這一行進行修改:

UPDATE Production.Products
  SET unitprice = unitprice + 1.00
WHERE productid = 2;

  這時該會話已被阻塞,因為修改操作鎖請求的排它鎖與前面會話授予的共享鎖有衝突。換句話說,如果讀操作是在未提交讀或已提交讀級別下執行的,那麼事務此時將不再持有共享鎖,Connection B嘗試修改改行的操作應該能夠成功。

  同樣,由於我設定了超時釋放時間,因此會有以下輸出:

  

  Step4.回到Connection A,執行以下程式碼,再次查詢茶品2所在的行,提交事務:

  SELECT productid, unitprice
  FROM Production.Products
  WHERE productid = 2;

COMMIT TRAN;

  這時的返回結果仍然與第一次相同:

  

  Step5.這時再執行Connection B中的更新語句,便能夠正常獲得排它鎖了,於是執行成功,價格變為了20.00。

可重複讀隔離級別不僅可以防止不可重複讀,另外還能防止丟失更新。丟失更新是指兩個事務讀取了同一個值,然後基於最初讀取的值進行計算,接著再更新該值,就會發生丟失更新的問題。這是因為在可重複讀隔離級別下,兩個事務在第一次讀操作之後都保留有共享鎖,所以其中一個都不能成功獲得為了更新資料而需要的排它鎖。但是,負面影響就是會導致死鎖

在可重複讀級別下執行的事務,讀操作獲得的共享鎖將一直保持到事務結束。因此可以保證在事務中第一次讀取某些行後,還可以重複讀取這些行。但是,事務只鎖定查詢第一次執行時找到的那些行,而不會鎖定查詢結果範圍外的其他行。因此,在同一事務進行第二次讀取之前,如果其他事務插入了新行,而且新行也能滿足讀操作額查詢過濾條件,那麼這些新行也會出現在第二次讀操作返回的結果中。這些新行稱之為幻影,這種讀操作也被稱為幻讀

3.4 SERIALIZEABLE 可序列化

  為了避免剛剛提到的幻讀,需要將隔離級別設定為可序列化。可序列化級別的處理方式與可重複讀類似:讀操作需要獲得共享鎖才能讀取資料並一直保留到事務結束,不同之處在於在可序列化級別下,讀操作不僅鎖定了滿足查詢條件的那些行,還鎖定了可能滿足查詢條件的行。換句話說,如果其他事務試圖增加能夠滿足操作的查詢條件的新行,當前事務就會阻塞這樣的操作。

  同樣,繼續來模擬:

  Step1.在Connection A中執行程式碼,設定隔離級別為可序列化,再查詢產品分類等於1的所有產品:

-- Connection A
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

BEGIN TRAN

  SELECT productid, productname, categoryid, unitprice
  FROM Production.Products
  WHERE categoryid = 1;

  

  Step2.在Connection B中執行程式碼,嘗試插入一個分類等於1的新產品:

-- Connection B
INSERT INTO Production.Products
    (productname, supplierid, categoryid,
     unitprice, discontinued)
  VALUES('Product ABCDE', 1, 1, 20.00, 0);

  這時,該操作會被阻塞。因為在可序列化級別下,前面的讀操作不僅鎖定了滿足查詢條件的那些行,還鎖定了可能滿足查詢條件的行。

  同樣,由於我設定了超時釋放時間,因此會有以下輸出:

  

  Step3.回到Connection A,執行以下程式碼,再次查詢分類1的產品,最後提交事務:

  SELECT productid, productname, categoryid, unitprice
  FROM Production.Products
  WHERE categoryid = 1;

COMMIT TRAN;

  Step4.回到Connection B,這時Connection B就已經獲得了等候已久的排它鎖,插入了新行。

INSERT INTO Production.Products
    (productname, supplierid, categoryid,
     unitprice, discontinued)
  VALUES('Product ABCDE', 1, 1, 20.00, 0);

SELECT productid, productname, categoryid, unitprice
FROM Production.Products
WHERE categoryid = 1;

  

  Step5.為了後面的演示,執行以下程式碼清理測試資料:

-- Cleanup
DELETE FROM Production.Products
WHERE productid > 77;

DBCC CHECKIDENT ('Production.Products', RESEED, 77);
View Code

3.5 SNAPSHOT 快照

  首先解釋一下什麼是快照?事務已經提交的行的上一個版本存在tempdb資料庫中,這是SQL Server引入的一個新功能。

  以這種行版本控制技術為基礎,SQL Server增加了兩個新的隔離級別:SNAPSHOT和READ COMMITED SNAPSHOT。如果啟用任何一種基於快照的隔離級別,DELETE和UPDATE語句在做出修改前都會把行的當前版本複製到tempdb資料庫中;INSERT語句則不會,因為這時還沒有行的舊版本。

  在SNAPSHOPT(快照)隔離級別下,當讀取資料時,可以保證讀操作讀取的行是事務開始時可用的最後提交的版本

  下面來模擬一下該隔離級別下的場景:

  Step1.還是開啟兩個會話視窗,在其中一個執行以下程式碼,設定隔離級別為SNAPSHOT:

-- Allow SNAPSHOT isolation in the database
ALTER DATABASE TSQLFundamentals2008 SET ALLOW_SNAPSHOT_ISOLATION ON;

  Step2.在Connection A中執行以下程式碼,更新產品2的價格,然後再查詢該產品的價格:

-- Connection A
BEGIN TRAN;

  UPDATE Production.Products
    SET unitprice = unitprice + 1.00
  WHERE productid = 2;

  SELECT productid, unitprice
  FROM Production.Products
  WHERE productid = 2;

  

  Step3.在Connection B中執行以下程式碼,設定隔離級別為SNAPSHOT,並查詢產品2的價格:

SET TRANSACTION ISOLATION LEVEL SNAPSHOT;

BEGIN TRAN;

  SELECT productid, unitprice
  FROM Production.Products
  WHERE productid = 2;

   這時的返回結果如下所示,可以看到這個結果是在該事務啟動時可用的最後提交的版本。

  

  Step4.回到Connection A提交這一修改的行:

COMMIT TRAN;

  Step5.在Connection B中執行以下程式碼,再次讀取資料,然後提交事務:

  SELECT productid, unitprice
  FROM Production.Products
  WHERE productid = 2;
  
COMMIT TRAN;

  然後我們會得到跟之前一樣的結果,奇了個怪了:

  

  但是如果我們再次在Connection B中執行以下完整語句:

BEGIN TRAN;

  SELECT productid, unitprice
  FROM Production.Products
  WHERE productid = 2;
  
COMMIT TRAN;

  這時結果便會同步,這個事務開始時可用的上一個提交的版本是價格=20.00

  

  為什麼兩個事務得到結果會不同?這是因為快照清理執行緒每隔一分鐘執行一次,現在由於沒有事務需要為價格=20.00的那個行版本了,所以清理執行緒下一次執行時會將這個行版本從tempdb資料庫中刪除掉。

  最後,為了下一次演示,清理測試資料:

-- Clear Test Data
UPDATE Production.Products
SET unitprice = 19.00
WHERE productid = 2;
View Code

這一隔離級別使用的不是共享鎖,而是行版本控制。如前所述,不論修改操作(主要是更新和刪除資料)是否在某種基於快照的隔離級別下的會話執行,快照隔離級別都會帶來效能上的開銷。

  另外,在SNAP快照級別下,可以通過檢查的行版本,檢測出更新衝突。它能判斷出在快照事務的一次讀操作和一次寫操作之間是否有其他事務修改過資料。如果SQL Server檢測到在讀取和寫入操作之間有另一個事務修改了資料,則會讓事務因失敗而終止,並返回以下錯誤資訊:

  

  衝突檢測完整例項如下:

---------------------------------------------------------------------
-- Conflict Detection 衝突檢測例項
---------------------------------------------------------------------

-- Connection A, Step 1
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;

BEGIN TRAN;

  SELECT productid, unitprice
  FROM Production.Products
  WHERE productid = 2;

-- Connection A, Step 2
  UPDATE Production.Products
    SET unitprice = 20.00
  WHERE productid = 2;
  
COMMIT TRAN;

-- Cleanup
UPDATE Production.Products
  SET unitprice = 19.00
WHERE productid = 2;

-- Connection A, Step 1
BEGIN TRAN;

  SELECT productid, unitprice
  FROM Production.Products
  WHERE productid = 2;

-- Connection B, Step 1
UPDATE Production.Products
  SET unitprice = 25.00
WHERE productid = 2;

-- Connection A, Step 2
  UPDATE Production.Products
    SET unitprice = 20.00
  WHERE productid = 2;

-- Cleanup
UPDATE Production.Products
  SET unitprice = 19.00
WHERE productid = 2;

-- Close all connections
View Code

3.6 READ COMMITED SNAPSHOT 已經提交讀隔離

  已提交讀隔離也是基於行版本控制,但與快照不同之處在於:在已提交讀級別下,讀操作讀取的資料行不是食物啟動之前最後提交的版本,而是語句啟動前最後提交的版本。

  此外,該級別不會像快照隔離級別一樣進行更新衝突檢測。這樣一來,它就跟SQL Server預設的READ COMMITED級別非常類似了,只不過讀操作不用獲得共享鎖,當請求的資源被其他事務的排它鎖鎖定時,也不用等待

  下面繼續通過案例來模擬:

  Step1.執行以下程式碼,設定隔離級別:

-- Turn on READ_COMMITTED_SNAPSHOT
ALTER DATABASE TSQLFundamentals2008 SET READ_COMMITTED_SNAPSHOT ON;

  執行該查詢需要一定的時間,並且要注意:要成功執行,當前連線必須是指定資料庫的唯一連線,請關掉其他連線,只保留一個會話來執行。

  可以看到它跟我們之前設定隔離級別所使用的的語句不同,這個選項其實就是把預設的READ COMMITED的寒意變成了READ COMMITED SNAPSHOT。意味著開啟這個選項時,除非顯式地修改會話的隔離級別,否則READ COMMITED SNAPSHOT將成為預設的隔離級別。

  Step2.在Connection A中執行以下程式碼,更新產品2所在的行記錄,再讀取這一行記錄,並且一直保持事務開啟:

-- Connection A
USE TSQLFundamentals2008;

BEGIN TRAN;

  UPDATE Production.Products
    SET unitprice = unitprice + 1.00
  WHERE productid = 2;

  SELECT productid, unitprice
  FROM Production.Products
  WHERE productid = 2;

  

  Step3.在Connection B中讀取產品2所在的行記錄,並一直保持事務開啟:

-- Connection B
BEGIN TRAN;

  SELECT productid, unitprice
  FROM Production.Products
  WHERE productid = 2;

  得到的結果是語句啟動之前最後提交的版本(19.00):

  

  Step4.回到Connection A,提交事務:

COMMIT TRAN;

  Step5.回到Connection B,再次讀取產品2所在的行,並提交事務:

  SELECT productid, unitprice
  FROM Production.Products
  WHERE productid = 2;

COMMIT TRAN;

  這時結果如下,可以看到跟SNAPSHOT不同,這次的結果是在語句執行之前最後提交的版本而不是事務執行之前最後提交的版本,因此得到了20.00:

  

回想一下,這種現象是不是我們常聽見的 不可重複讀?也就是說,該級別下,無法防止不可重複讀問題。

  最後,按照國際慣例,清理測試資料:

-- Clear Test Data
UPDATE Production.Products
SET unitprice = 19.00
WHERE productid = 2;
View Code

  然後,關閉所有連線,然後在一個新的連線下執行以下程式碼,以禁用指定資料庫的基於快照的隔離級別:(執行ALTER DATABASE TSQLFundamentals2008 SET READ_COMMITTED_SNAPSHOT OFF;這一句時可能需要花費一點時間,請耐心等候;)

-- Make sure you're back in default mode
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- Change database options to default
ALTER DATABASE TSQLFundamentals2008 SET ALLOW_SNAPSHOT_ISOLATION OFF;
ALTER DATABASE TSQLFundamentals2008 SET READ_COMMITTED_SNAPSHOT OFF;
View Code

3.7 隔離級別總結

  下表總結了每種隔離級別能夠解決各種邏輯一致性的問題,以及隔離級別是否會檢測更新衝突,是否使用了行版本控制。

  這時再回顧以下各個問題的描述及結果,我們來看另一個表:

併發事務引起的問題

      問題          

                    描述                      

              結果             

                          解決                     

丟失更新

A讀—B讀—A改—B改

A更改丟失

READ UNCOMMITTED

髒讀

A改—B讀—A回滾

B讀無效值

READ COMMITTED

不可重讀

相關推薦

no