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

本篇索引:html

一、事務程序員

二、鎖定和阻塞sql

三、隔離級別數據庫

四、死鎖服務器

1、事務

1.1 事務的概念

  事務是做爲單個工做單元而執行的一系列操做,好比查詢和修改數據等。session

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

好比網上訂火車票,要麼你定票成功,餘票顯示就減一張; 要麼你定票失敗獲取取消訂票,餘票的數量仍是那麼多。不容許出現你訂票成功了,餘票沒有減小或者你取消訂票了,餘票顯示卻少了一張的這種狀況。這種不被容許出現的狀況就要求購票和餘票減小這兩個不一樣的操做必須放在一塊兒,成爲一個完整的邏輯鏈,這樣就構成了一個事務。ide

1.2 事務的ACID特性

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

  一致性(Consistency):能夠理解爲數據的完整性,事務的提交要確保在數據庫上的操做沒有破壞數據的完整性,好比說不要違背一些約束的數據插入或者修改行爲。一旦破壞了數據的完整性,SQL Server 會回滾這個事務來確保數據庫中的數據是一致的。post

  隔離性(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、鎖定和阻塞

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):

  

3、隔離級別

  隔離級別用於決定如何控制併發用戶讀寫數據的操做。前面說到,讀操做默認使用共享鎖,寫操做須要使用排它鎖。對於操做得到的鎖,以及鎖的持續時間來講,雖然不能控制寫操做的處理方式,但能夠控制讀操做的處理方式。做爲對讀操做的行爲進行控制的一種結果,也會隱含地影響寫操做的行爲方式。

  爲此,能夠在會話級別上用會話選項來設置隔離級別,也能夠在查詢級別上用表提示(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

不可重讀

A讀—B改—A讀

A讀不一致

REPEATABLE READ

不可重讀

A讀—B改—A讀

A讀不一致

SNAPSHOT

幻讀

A讀—B增刪—A讀

A讀或多或少

SERIALIZABLE

4、死鎖

4.1 死鎖是個什麼鬼?

  死鎖是指一種進程之間互相永久阻塞的狀態,可能涉及到兩個或者多個進程。兩個進程發生死鎖的例子是:進程A阻塞了進程B,進程B又阻塞了進程A。在任何一種狀況下,SQL Server均可以檢測到死鎖,並選擇終止其中一個事務以干預死鎖狀態。若是SQL Server不干預,那麼死鎖涉及到的進程將會永遠保持死鎖狀態。

  默認狀況下,SQL Server會選擇終止作過的操做最少的事務,由於這樣可讓回滾開銷下降到最低。固然,在SQL Server 2005及以後的版本中,能夠經過將會話選項DEADLOCK_PRIORITY設置爲範圍(-10到10)之間的任一整數值。

4.2 死鎖實例

  仍然打開三個會話:Connection A、B和C:

  Step1.在Connection A中更新Products表中產品2的行記錄,並保持事務一直打開:

-- Connection A
USE TSQLFundamentals2008;

BEGIN TRAN;

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

  這時Connection A對產品表的產品2請求了排它鎖。

  Step2.在Connection B中更新OrderDetails表中產品2的訂單明細,並保持事務一直打開:

-- Connection 2
BEGIN TRAN;

  UPDATE Sales.OrderDetails
    SET unitprice = unitprice + 1.00
  WHERE productid = 2;

  這時Connection A對訂單明細表的產品2請求了排它鎖。

  Step3.回到Connection A中,執行如下語句,請求查詢產品2的訂單明細記錄:

-- Connection A

  SELECT orderid, productid, unitprice
  FROM Sales.OrderDetails
  WHERE productid = 2;

COMMIT TRAN;

  因爲此時實在默認的READ COMMITED隔離級別下運行的,因此Connection A中的事務須要一個共享鎖才能讀數據,所以這裏會一直阻塞住。可是,此時並無發生死鎖,而只是發生了阻塞。

  Step4.回到Connection B中,執行如下語句,嘗試在Products表查詢產品2的記錄:

-- Connection 2

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

COMMIT TRAN;

  這裏因爲這個請求和Connection A中的事務在同一個資源上持有的排它鎖發生了衝突,因而相互阻塞發生了死鎖。SQL Server一般會在幾秒鐘以內檢測到死鎖,並從這兩個進程中選擇一個做爲犧牲品,終止其事務。因此咱們仍是獲得瞭如下結果:

  

  Step5.剛剛提到了SQL Server會選擇一個做爲犧牲品,咱們回到Connection A會看到如下的錯誤信息提示:

  

  在這個例子中,因爲兩個事務進行的工做量差很少同樣,因此任何一個事務都有可能被終止。(前面提到,若是沒有手動設置優先級,那麼SQL Server會選擇工做量較小的一個事務做爲犧牲品)另外,解除死鎖須要必定的系統開銷,由於這個過程會涉及撤銷已經執行過的處理。

顯然,事務處理的時間越長,持有鎖的時間也就越長,死鎖的可能性也就越大。應該儘可能保持事務簡短,把邏輯上能夠屬於同一工做單元的操做移到事務以外。

4.3 避免死鎖

  (1)改變訪問資源的順序能夠避免死鎖

  繼續上面的例子,Connection A先訪問Products表中的行,而後訪問OrderDetails表中的行;Connection B先訪問OrderDetails表中的行,而後訪問Products表中的行。

  這時若是咱們改變一下訪問順序:兩個事務按照一樣的順序來訪問資源,則不會發生這種類型的死鎖。

經過交換其中一個事務的操做順序,就能夠避免發生這種類型的死鎖(假設交換順序沒必要改變程序的邏輯)。 

  (2)良好的索引設計也能夠避免死鎖

  若是查詢篩選條件缺乏良好的索引支持,也會形成死鎖。例如,假設Connection B中的事務有兩條語句要對產品5進行篩選,Connection A中的事務要對產品2進行處理,那麼他們就不該該有任何衝突。可是,若是在表的productid列上若是沒有索引來支持查詢篩選,那麼SQL Server就必須掃描(並鎖定)表中的全部行,這樣固然會致使死鎖。

總之,良好的索引設計將有助於減小這種沒有真正的邏輯衝突的死鎖。

  最後,按照國際慣例清理掉測試數據:

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

UPDATE Sales.OrderDetails
  SET unitprice = 19.00
WHERE productid = 2
  AND orderid >= 10500;

UPDATE Sales.OrderDetails
  SET unitprice = 15.20
WHERE productid = 2
  AND orderid < 10500;
View Code

5、小結

  本篇介紹了事務和併發,重點解釋了事務是個什麼鬼,以及在SQL Server中如何管理事務。演示了在SQL Server中如何把一個事務訪問的數據和其餘事務的不一致性使用進行隔離,以及如何處理死鎖的狀況。相信隨着這些內容的理解,咱們對事務和併發的認知再也不停留在數據庫基礎的教材裏邊,也但願對你們有所幫助。最後推薦各位.NET程序員都閱讀一下《MS SQL Server 2008技術內幕:T-SQL語言基礎》這本書,真的是值得閱讀的一本。

  後續我會閱讀《MS SQL Server 2008技術內幕:T-SQL查詢》,會帶來更多的分享給你們!

參考資料

TSQLFundenmantals

(1)[美] Itzik Ben-Gan 著,成保棟 譯,《Microsoft SQL Server 2008技術內幕:T-SQL語言基礎》

考慮到不少人買了這本書,卻下載不了這本書的配套源代碼和示例數據庫,特地上傳到了百度雲盤中,點此下載

(2)BIWORK,《SQL Server 中的事務與事務隔離級別以及如何理解髒讀, 未提交讀,不可重複讀和幻讀產生的過程和緣由

(3)Jackson,《30分鐘全面解析-SQL事務+隔離級別+阻塞+死鎖

 

相關文章
相關標籤/搜索