Microsoft SQL Server中的事务(转载)
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.00WHERE 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 explorerequest_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 exploresession_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_connectionsCROSS 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,此時會得到以下輸出:
在已提交讀級別下,不會讀取臟數據,只能讀取已經提交過的修改。但是,該級別下,其他事務可以在兩個讀操作之間更改數據資源,讀操作因而可能每次得到不同的取值。這種現象被稱為?不可重復讀?! ?/p>
3.3 REPEATABLE READ 可重復讀
如果想保證在事務內進行的兩個讀操作之間,其他任何事務都不能修改由當前事務讀取的數據,則需要將隔離級別升級為可重復讀。在該級別下,十五中的讀操作不但需要獲得共享鎖才能讀數據,而且獲得的共享鎖將一直保持到事務完成為止。換句話說,在事務完成之前,沒有其他事務能夠獲得排它鎖以修改這一數據資源,由此來保證實現可重復的讀取。
Step1.為了重新演示可重復讀的示例,首先需要將剛剛的測試數據清理掉,在Connection A和B中執行以下代碼:
?View CodeStep2.在Connection A中運行以下代碼,將會話的隔離級別設置為可重復讀,再查詢產品2所在的行記錄:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;BEGIN TRAN;SELECT productid, unitpriceFROM Production.ProductsWHERE productid = 2;
這時該會話仍然持有產品2上的共享鎖,因為在該隔離級別下,共享鎖要一直保持到事務結束為止。
Step3.在Connection B中嘗試對產品2這一行進行修改:
UPDATE Production.ProductsSET unitprice = unitprice + 1.00 WHERE productid = 2;這時該會話已被阻塞,因為修改操作鎖請求的排它鎖與前面會話授予的共享鎖有沖突。換句話說,如果讀操作是在未提交讀或已提交讀級別下運行的,那么事務此時將不再持有共享鎖,Connection B嘗試修改改行的操作應該能夠成功。
同樣,由于我設置了超時釋放時間,因此會有以下輸出:
Step4.回到Connection A,運行以下代碼,再次查詢茶品2所在的行,提交事務:
SELECT productid, unitpriceFROM Production.ProductsWHERE productid = 2;COMMIT TRAN;這時的返回結果仍然與第一次相同:
Step5.這時再執行Connection B中的更新語句,便能夠正常獲得排它鎖了,于是執行成功,價格變為了20.00。
可重復讀隔離級別不僅可以防止不可重復讀,另外還能防止丟失更新。丟失更新是指兩個事務讀取了同一個值,然后基于最初讀取的值進行計算,接著再更新該值,就會發生丟失更新的問題。這是因為在可重復讀隔離級別下,兩個事務在第一次讀操作之后都保留有共享鎖,所以其中一個都不能成功獲得為了更新數據而需要的排它鎖。但是,負面影響就是會導致死鎖。
在可重復讀級別下運行的事務,讀操作獲得的共享鎖將一直保持到事務結束。因此可以保證在事務中第一次讀取某些行后,還可以重復讀取這些行。但是,事務只鎖定查詢第一次運行時找到的那些行,而不會鎖定查詢結果范圍外的其他行。因此,在同一事務進行第二次讀取之前,如果其他事務插入了新行,而且新行也能滿足讀操作額查詢過濾條件,那么這些新行也會出現在第二次讀操作返回的結果中。這些新行稱之為幻影,這種讀操作也被稱為幻讀。
3.4 SERIALIZEABLE 可序列化
為了避免剛剛提到的幻讀,需要將隔離級別設置為可序列化??尚蛄谢墑e的處理方式與可重復讀類似:讀操作需要獲得共享鎖才能讀取數據并一直保留到事務結束,不同之處在于在可序列化級別下,讀操作不僅鎖定了滿足查詢條件的那些行,還鎖定了可能滿足查詢條件的行。換句話說,如果其他事務試圖增加能夠滿足操作的查詢條件的新行,當前事務就會阻塞這樣的操作。
同樣,繼續來模擬:
Step1.在Connection A中運行代碼,設置隔離級別為可序列化,再查詢產品分類等于1的所有產品:
-- Connection A SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;BEGIN TRANSELECT productid, productname, categoryid, unitpriceFROM Production.ProductsWHERE 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, unitpriceFROM Production.ProductsWHERE 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.為了后面的演示,運行以下代碼清理測試數據:
?View Code3.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.ProductsSET unitprice = unitprice + 1.00WHERE productid = 2;SELECT productid, unitpriceFROM Production.ProductsWHERE productid = 2;
Step3.在Connection B中運行以下代碼,設置隔離級別為SNAPSHOT,并查詢產品2的價格:
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;BEGIN TRAN;SELECT productid, unitpriceFROM Production.ProductsWHERE productid = 2;?這時的返回結果如下所示,可以看到這個結果是在該事務啟動時可用的最后提交的版本。
Step4.回到Connection A提交這一修改的行:
COMMIT TRAN;Step5.在Connection B中運行以下代碼,再次讀取數據,然后提交事務:
SELECT productid, unitpriceFROM Production.ProductsWHERE productid = 2;COMMIT TRAN;然后我們會得到跟之前一樣的結果,奇了個怪了:
但是如果我們再次在Connection B中運行以下完整語句:
BEGIN TRAN;SELECT productid, unitpriceFROM Production.ProductsWHERE productid = 2;COMMIT TRAN;這時結果便會同步,這個事務開始時可用的上一個提交的版本是價格=20.00
為什么兩個事務得到結果會不同?這是因為快照清理線程每隔一分鐘運行一次,現在由于沒有事務需要為價格=20.00的那個行版本了,所以清理線程下一次運行時會將這個行版本從tempdb數據庫中刪除掉。
最后,為了下一次演示,清理測試數據:
?View Code這一隔離級別使用的不是共享鎖,而是行版本控制。如前所述,不論修改操作(主要是更新和刪除數據)是否在某種基于快照的隔離級別下的會話執行,快照隔離級別都會帶來性能上的開銷。
另外,在SNAP快照級別下,可以通過檢查的行版本,檢測出更新沖突。它能判斷出在快照事務的一次讀操作和一次寫操作之間是否有其他事務修改過數據。如果SQL Server檢測到在讀取和寫入操作之間有另一個事務修改了數據,則會讓事務因失敗而終止,并返回以下錯誤信息:
沖突檢測完整實例如下:
?View Code3.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.ProductsSET unitprice = unitprice + 1.00WHERE productid = 2;SELECT productid, unitpriceFROM Production.ProductsWHERE productid = 2;
Step3.在Connection B中讀取產品2所在的行記錄,并一直保持事務打開:
-- Connection B BEGIN TRAN;SELECT productid, unitpriceFROM Production.ProductsWHERE productid = 2;得到的結果是語句啟動之前最后提交的版本(19.00):
Step4.回到Connection A,提交事務:
COMMIT TRAN;Step5.回到Connection B,再次讀取產品2所在的行,并提交事務:
SELECT productid, unitpriceFROM Production.ProductsWHERE productid = 2;COMMIT TRAN;這時結果如下,可以看到跟SNAPSHOT不同,這次的結果是在語句執行之前最后提交的版本而不是事務執行之前最后提交的版本,因此得到了20.00:
回想一下,這種現象是不是我們常聽見的?不可重復讀?也就是說,該級別下,無法防止不可重復讀問題。
最后,按照國際慣例,清理測試數據:
?View Code然后,關閉所有連接,然后在一個新的連接下運行以下代碼,以禁用指定數據庫的基于快照的隔離級別:(執行ALTER DATABASE TSQLFundamentals2008 SET READ_COMMITTED_SNAPSHOT OFF;這一句時可能需要花費一點時間,請耐心等候;)
?View Code3.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.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.ProductsSET unitprice = unitprice + 1.00WHERE productid = 2;這時Connection A對產品表的產品2請求了排它鎖。
Step2.在Connection B中更新OrderDetails表中產品2的訂單明細,并保持事務一直打開:
-- Connection 2 BEGIN TRAN;UPDATE Sales.OrderDetailsSET unitprice = unitprice + 1.00WHERE productid = 2;這時Connection A對訂單明細表的產品2請求了排它鎖。
Step3.回到Connection A中,執行以下語句,請求查詢產品2的訂單明細記錄:
-- Connection ASELECT orderid, productid, unitpriceFROM Sales.OrderDetailsWHERE productid = 2;COMMIT TRAN;由于此時實在默認的READ COMMITED隔離級別下運行的,所以Connection A中的事務需要一個共享鎖才能讀數據,因此這里會一直阻塞住。但是,此時并沒有發生死鎖,而只是發生了阻塞。
Step4.回到Connection B中,執行以下語句,嘗試在Products表查詢產品2的記錄:
-- Connection 2SELECT productid, unitpriceFROM Production.ProductsWHERE 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就必須掃描(并鎖定)表中的所有行,這樣當然會導致死鎖。
總之,良好的索引設計將有助于減少這種沒有真正的邏輯沖突的死鎖。
最后,按照國際慣例清理掉測試數據:
?View Code五、小結
本篇介紹了事務和并發,重點解釋了事務是個什么鬼,以及在SQL Server中如何管理事務。演示了在SQL Server中如何把一個事務訪問的數據和其他事務的不一致性使用進行隔離,以及如何處理死鎖的情況。相信隨著這些內容的理解,我們對事務和并發的認知不再停留在數據庫基礎的教材里邊,也希望對大家有所幫助。最后推薦各位.NET程序員都閱讀一下《MS SQL Server 2008技術內幕:T-SQL語言基礎》這本書,真的是值得閱讀的一本。
后續我會閱讀《MS SQL Server 2008技術內幕:T-SQL查詢》,會帶來更多的分享給大家!
參考資料
(1)[美] Itzik Ben-Gan 著,成保棟 譯,《Microsoft SQL Server 2008技術內幕:T-SQL語言基礎》
考慮到很多人買了這本書,卻下載不了這本書的配套源代碼和示例數據庫,特意上傳到了百度云盤中,點此下載
(2)BIWORK,《SQL Server 中的事務與事務隔離級別以及如何理解臟讀, 未提交讀,不可重復讀和幻讀產生的過程和原因》
(3)Jackson,《30分鐘全面解析-SQL事務+隔離級別+阻塞+死鎖》
轉載于:https://www.cnblogs.com/fanxiumin/p/6130710.html
總結
以上是生活随笔為你收集整理的Microsoft SQL Server中的事务(转载)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: PHP之factory
- 下一篇: GDB调试汇编堆栈过程分析