原本打算寫有關 SSIS Package 中的事務控制過程的,但是發現很多基本的概念還是需要有 SQL Server 事務和事務的隔離級别做基礎鋪墊。是以花了點時間,把 SQL Server 資料庫中的事務概念,ACID 原則,事務中常見的問題,問題造成的原因和事務隔離級别等這些方面的知識好好的整理了一下。
其實有關 SQL Server 中的事務,說實話因為内容太多, 話題太廣,稍微力度控制不好就超過了我目前知識能力範圍,就不是三言兩語能夠講清楚的。是以希望大家能夠指出其中總結的不足之處,對我來說多了提高的機會,更可以幫助大家加深對事務的了解。
本文涉及到的知識點:
SQL Server 資料庫中事務的概念
ACID 原則 (加了一部分内容專門解釋原子性,提到了顯示事務以及 XACT_ABORT 機制來確定事務的原子性)
列出事務中常見的問題以及原因:髒讀,未送出讀,不可重複讀,幻讀 等
SQL Server中 事務的隔離級别以及它們如何做到避免髒讀,未送出讀,不可重複讀和幻讀 (用代碼描述了這些問題,并且使用時間序來解釋産生的原因)
資料庫中的事務是資料庫并發控制的基本機關,一條或者一組語句要麼全部成功,對資料庫中的某些資料成功修改; 要麼全部不成功,資料庫中的資料還原到這些語句執行
之前的樣子。比如網上訂火車票,要麼你定票成功,餘票顯示就減一張; 要麼你定票失敗擷取取消訂票,餘票的數量還是那麼多。不允許出現你訂票成功了,餘票沒有減少或者你取消訂票了,餘票顯示卻少了一張的這種情況。這種不被允許出現的情況就要求購票和餘票減少這兩個不同的操作必須放在一起,成為一個完整的邏輯鍊,這樣就構成了一個事務。
資料庫中事務的 ACID 原則
原子性 (Atomicity):事務的原子性是指一個事務中包含的一條語句或者多條語句構成了一個完整的邏輯單元,這個邏輯單元具有不可再分的原子性。這個邏輯單元要麼一起送出執行全部成功,要麼一起送出執行全部失敗。
一緻性 (Consistency):可以了解為資料的完整性,事務的送出要確定在資料庫上的操作沒有破壞資料的完整性,比如說不要違背一些限制的資料插入或者修改行為。一旦破壞了資料的完整性,SQL Server 會復原這個事務來確定資料庫中的資料是一緻的。
隔離性(Isolation):與資料庫中的事務隔離級别以及鎖相關,多個使用者可以對同一資料并發通路而又不破壞資料的正确性和完整性。但是,并行事務的修改必須與其它并行事務的修改互相獨立,隔離。 但是在不同的隔離級别下,事務的讀取操作可能得到的結果是不同的。
持久性(Durability):資料持久化,事務一旦對資料的操作完成并送出後,資料修改就已經完成,即使服務重新開機這些資料也不會改變。相反,如果在事務的執行過程中,系統服務崩潰或者重新開機,那麼事務所有的操作就會被復原,即回到事務操作之前的狀态。
我了解在極端斷電或者系統崩潰的情況下,一個發生在事務未送出之前,資料庫應該記錄了這個事務的"ID"和部分已經在資料庫上更新的資料。供電恢複資料庫重新啟動之後,這時完成全部撤銷和復原操作。如果在事務送出之後的斷電,有可能更改的結果沒有正常寫入磁盤持久化,但是有可能丢失的資料會通過事務日志自動恢複并重新生成以寫入磁盤完成持久化。
原子性的進一步了解
關于原子性,有必要在這裡多補充一下,因為我們描述的概念是指在事務中的原子性。一條 SQL 語句和多條 SQL 語句在處理原子性上是有一些差別的,下面示範了這些差別。
先運作這些代碼,建立一個非常簡單的測試表,這張表隻簡單模拟了一個賬戶的 ID 和賬戶餘額。
單條 SQL 語句的原子性
插入一條測試語句,然後再查詢一下結果。

這裡提到了自動送出事務,這時 T-SQL 預設的事務方式,它是一種能夠自動執行并能夠自動復原事務的處理方式。SQL Server 除了自動送出事務之外,還有顯示事務和隐式事務,暫時不在這篇文章中讨論它們的差別了。
上面的兩個自動送出事務中,每一個自動送出事務隻包含一條 SQL 語句,不能再分,要麼成功,要麼失敗。
再比如,在一條 SQL 語句中插入多條資料時,其中一條資料是符合限制的。但因為另外一條資料違反了檢查限制,這樣也會導緻整個 Insert 語句失敗,是以沒有一條資料能夠插入到資料表中。
多條 SQL 語句形成的一個整體的原子性
假設下面的這兩條 Insert 語句構成一個具備原子性特征的邏輯單元,是一個整體需要形成一個事務,那麼應該如何處理。
很顯然如果直接這麼執行的話,1004 插入失敗,1005 可以插入成功,這樣就是兩個不同的事務了。SQL Server 提供了兩種方式來確定這種包含多組 SQL 語句的邏輯塊具備原子性特征。
方式一 - 使用顯示事務組合多條 SQL 語句構成一個整體以實作事務的原子性
第一種就是非常常見的顯示事務,通過顯示的使用 BEGIN TRANSACTION, COMMIT TRANSACTION 以及 ROLLBACK TRANSACTION 指令将一組 SQL 語句形成一個完整的事務來送出,送出要麼成功,要麼失敗。
當然最終的結果就是事務復原,一條資料都沒有插入到資料表中,是以失敗時就全部失敗,確定了事務的原子性。
方式二 - 通過設定 XACT_ABORT 為 ON 來確定事務的原子性
先來看預設的設定,當 XACT_ABORT 為 OFF 狀态的時候。
當 XACT_ABORT 為 OFF 狀态即 SQL Server 預設設定下,上面的事務中,SQL Server 在通常情況下隻會復原執行失敗的語句,也就是說隻會復原 1004 這條資料,而 1005 會插入成功。很顯然,這違背了事務的原子性,因為我們也沒有顯示的寫出要 ROLLBACK TRANSACTION 來。
OK!那我們将 XACT_ABORT 設定為 ON,這時就告訴了它後面的事務,如果遇到錯誤就立即終止事務并復原。這樣不通過顯示的 ROLLBACK TRANSACTION 也可以確定事務的原子性。
在上面的這個例子中,隻有事務 2 會成功送出,而事務1和3會復原,插入操作執行失敗。
注意一點,上面的每個事務後面加了一個 GO 關鍵字,如果不加 GO 這個關鍵字,一起執行這些 SQL 語句會導緻事務2和3因為事務1的執行失敗而不能執行到, GO 關鍵字形成了一個批處理,表示前面的一組 SQL 語句一起處理。
GO 關鍵字非常有意思,GO 後面可以加上次數,表示前面的一條或者一組 SQL 執行幾次。
通過上面的示例,應該可以了解原子性與事務的關系了,以及如何實作事務的原子性。
事務中常見的問題
了解完事務的 ACID 的原則後,再來看看在 SQL Server 中多使用者并發的情況下,使用事務可能會遇到的一些情況:
髒讀 (Dirty Reads) : 一個事務正在通路并修改資料庫中的資料但是沒有送出,但是另外一個事務可能讀取到這些已作出修改但未送出的資料。這樣可能導緻的結果就是所有的操作都有可能復原,比如第一個事務對資料做出的修改可能違背了資料表的某些限制,破壞了完整性,但是恰巧第二個事務卻讀取到了這些不正确的資料造成它自身操作也發生失敗復原。
不可重複讀取(Non-Repeatable Reads): A 事務兩次讀取同一資料,B事務也讀取這同一資料,但是 A 事務在第二次讀取前B事務已經更新了這一資料。是以對于A事務來說,它第一次和第二次讀取到的這一資料可能就不一緻了。
幻讀(Phantom Reads): 與不可重複讀有點類似,都是兩次讀取,不同的是 A 事務第一次操作的比如說是全表的資料,此時 B 事務并不是隻修改某一具體資料而是插入了一條新資料,而後 A 事務第二次讀取這全表的時候就發現比上一次多了一條資料,發生幻覺了。
更新丢失(Lost Update): 兩個事務同時更新,但由于某一個事務更新失敗發生復原操作,這樣有可能的結果就是第二個事務已更新的資料因為第一個事務發生復原而導緻資料最終沒有發生更新,是以兩個事務的更新都失敗了。
SQL Server 中事務的隔離級别以及與髒讀,不可重複讀,幻讀等關系(代碼論證和時間序)
了解了在并發通路資料庫的情況下可能會出現這些問題,就可以繼續了解資料庫隔離級别這樣的一個概念,通俗一點講就是:你希望通過何種方式讓并發的事務隔離開來,隔離到什麼程度?比如可以容忍髒讀,或者不希望并發的事務出現髒讀的情況,那麼這些可以通過隔離級别的設定使得并發事務之間的隔離程度變得寬松或者很嚴峻。
隔離級别越高,讀取髒資料或者造成資料不統一不完整的機會就越少,但是在高并發的系統中,性能降低就越嚴重。隔離級别越低,并發系統中性能上提升很大,但是資料本身可能不完整。
在 SQL Server 2012 中可以通過這樣的文法來設定事務的隔離級别 (從低到高排列):
下面通過代碼示例來示範各個事務隔離級别的表現,運作下面 SQL 語句,插入一條測試語句。
Read Uncommitted (未送出讀)
隔離級别最低,容易産生的問題就是髒讀,因為可以讀取其它事務修改了的但是沒有送出的資料。它的作用跟在事務中 SELECT 語句對象表上設定 (NOLOCK) 相同。
打開兩個查詢視窗,第一個視窗表示事務 A, 第二個視窗表示事務B。 事務A 保持預設的隔離級别,事務B 設定它們的隔離級别為 READ UNCOMMITTED, 可以通過 DBCC USEROPITIONS 檢視更改後的結果。
測試步驟:
先執行事務 A 的 SQL 代碼
馬上接着再執行 事務 B 的 SQL 代碼
可以看出,事務 B 對 ID = 1001 的這條資料進行了兩次讀取,但是很顯然第一次讀取的資料是髒資料。下面模拟了一下它們發生的時序,雖然不算嚴謹,但是可以幫助了解髒讀産生的原因。
還可以把事務B 的隔離級别改回來成為預設的 READ COMMITTED,然後運作完事務 A 之後馬上運作帶有 NOLOCK 的查詢,效果和上面描述的也是一緻的。 一旦加上 NOLOCK,可以認為它的作用就等同于隔離級别為 READ UNCOMMITTED。
Read Committed (已送出讀)
這是 SQL Server 的預設設定,已送出讀,可以避免髒讀,可以滿足大多數要求。事務中的語句不能讀取已由其它事務做出修改但是還未送出的資料,但是能夠讀取由其它事務做出修改并送出了的資料。也就是說,有可能會出現 Non-Repeatable Reads 不可重複讀取和 Phantom Reads 幻讀的情況,因為目前事務中可能出現兩次讀取同一資源,但是兩次讀取的過程之間,另外一事務可能對這一資源完成了讀取更新并送出的行為,這樣資料前後可能就不一緻了。是以,這一個預設的隔離級别能夠解決髒讀但是解決不了 Non-Repeatable Reads 不可重複讀。
接着上一個例子,看看如果将隔離級别設定為 READ COMMITTED,能否避免髒讀? 還是先運作事務 A,再接着運作事務 B。
因為已送出讀不能讀取已由其它事物做出修改但是還未送出的資料,是以事務B 就必須等待事務 A 完成對資料的修改送出或者復原之後才能開始讀取。運作事務A 和事務B,明顯事務B 有一個等待事務A送出或者復原的過程,看看它們的時序圖。
由此可以看出隔離級别 READ COMMITTED 可以避免髒讀,但是也有可能出現其它的問題,請看這個例子。先執行事務A,接着直接執行事務 B。
從上面的執行結果來看,很明顯在事務 A 中,同一個事務中對 ID = 1001 的取值出現了前後不一緻的情況。假設這裡不是簡單的查詢,而是先查詢賬戶餘額有 1000元錢,然後後面的動作就是取 1000元錢,很明顯第二次取的時候發現隻有 500 元了。原因就是在第一次查詢和取的間隙之間被事務 B 鑽了空子,修改了餘額。這種情況就是上面所介紹到的不可重複讀取,請看下面的時序圖。
是以 READ COMMITTED 已送出讀隔離級别能夠避免髒讀,但是仍然會遇到不可重複讀取的問題。
Repeatable Read (可重複讀)
不能讀取已由其它事務修改了但是未送出的行,其它任何事務也不能修改在目前事務完成之前由目前事務讀取的資料。但是對于其它事務插入的新行資料,目前事務第二次通路表行時會檢索這一新行。是以,這一個隔離級别的設定解決了 Non-Repeatable Reads 不可重複讀取的問題,但是避免不了 Phantom Reads 幻讀。
接着上面的例子做出一些修改,增加了一些查詢,記得把 ID = 1001 的餘額改回 1000。将事務 A 的隔離級别設定為 REPEATABLE READ 可重複讀級别,來看看這個隔離級别的表現。
盡管在最後的查詢結果中, ID = 1001 的餘額為 500 元,但是在事務 A 中的兩次讀取一次發生在 事務 B 開始之前,一次發生在 事務 B 送出之後,但是它們讀取的餘額是保持一緻的,看不到事務 B 對這個值的修改。
從上面的時序圖中可以看出,事務 A 第一次讀取到的 ID = 1001 的餘額值和第二次讀取到的是一樣的,可以了解為在事務 A 的查詢期間是不允許事務 B 修改這個值的。 因為事務 A 确實沒有看到這個變化,是以事務A 也确實認為事務B 聽了它的話,沒有做出 Update 的操作。但是實際上,事務 B 已經完成了這個操作,隻不過由于 事務 A 中隔離級别設定為 REPEATABLE READ 可重複讀,是以兩次讀取的結果始終保持着一緻。
那麼這裡的示例是事務B在修改資料,如果是新增加一行記錄呢?
事務 A 又開始暈菜了!居然兩次查詢的結果不一樣,第二次查詢多了一條資料,這就是幻讀!
SNAPSHOT (快照隔離)
可以解決幻讀 Phantom Reads 的問題,目前事務中讀取的資料在整個事務開始到事務送出結束之間,這個資料版本是一緻的。其它的事務可能對這些資料做出修改,但是對于目前事務來說它是看不到這些變化。有點類似于目前事務拿到這個資料的時候是拿到這個資料的快照,是以在這個快照上做出的操作同一事務中前後幾次操作都是基于同一資料版本。是以,這一個隔離級别的設定可以解決 Phantom Reads 幻讀問題。但是要注意的是,其它事務是可以在目前事務完成之前修改由目前事務讀取的資料。
在使用 SNAPSHOT 之前要注意,預設情況下資料庫不允許設定 SNAPSHOT 隔離級别,直接設定會出現類似于這樣的錯誤:
DBCC execution completed. If DBCC printed error messages, contact your system administrator.
Msg 3952, Level 16, State 1, Line 8
Snapshot isolation transaction failed accessing database 'BIWORK_SSIS' because snapshot isolation is not allowed in this database. Use ALTER DATABASE to allow snapshot isolation.
是以要使用 SET 指令開啟這個支援
并且在開始前先清空其它的 ID,隻保留 ID = 1001 的這條記錄。
這樣通過設定隔離級别是 SNAPSHOT就解決了幻讀的問題,保證了在事務 A 中查詢的資料行版本是前後一緻的。
但是大家發現沒有?無論在事務 A 中使用 Repeatable Read 還是 Snapshot 仍然不可避免的阻止事務B 對共享的資源做出了修改,盡管這個修改沒有被事務 A 發現,事務 A 中的資料還是保持了一緻,但是實際上還是做出了修改。隻要事務 A 一送出結束,馬上就可以看到事務 B 做出的這些修改已經生效了。回顧之前提到的,如果我第一次查詢有1000元,第二次動作可能就是取1000元。在這兩次動作之間另外的一個事務對金額做出了修改,盡管我兩次讀取都是1000元,但是實際上是不符合常理的。要麼,我先查詢然後再取款這個動作是連貫的,然後另外一個事務再對金額做出修改。要麼,其它事務先對金額做出修改,比如扣去500元,那麼我再查詢再取款這個錢數還是一緻的。也就是說,在事務 A 對某一個資源做出操作的時候,形成了獨占,事務 B 進不來。或者事務 B 在對這個資源做操作的時候,事務 A 也必須等待事務 B 結束後才能開始它的事務,那麼這裡就要使用到最嚴格的隔離級别了 - SERIALIZABLE。
SERIALIZABLE(序列化)
性能最低,隔離級别最高最嚴格,可以幾乎上面提到的所有問題。比如不能讀取其它已由其它事務修改但是沒有送出的資料,不允許其它事務在目前事務完成修改之前修改由目前事務讀取的資料,不允許其它事務在目前事務完成修改之前插入新的行。它的作用與在事務内所有 SELECT 語句中的所有表上設定 HOLDLOCK 相同,并發級别比較低但又對安全性要求比較高的時候可以考慮使用。如果并發級别很高,使用這個隔離級别,性能瓶頸将非常嚴重。
将事務 A 的隔離級别調整成 SERIALIZABLE,然後執行 A 然後再執行 B。
在這裡可以看到事務B 的執行基本上是在事務A送出之後才開始的,當事務 A 在執行的時候,事務 B 因為也要通路這個資源是以一直阻塞在那裡直到事務 A 送出。 并不是說事務 B 沒有開始,而是說在執行 SELECT 查詢的時候因為事務 A 占用了這個資源,是以處于等待狀态。
在 SQL Server 中設定隔離級别要注意:一次隻能設定一個隔離級别的選項,并且設定的隔離級别對目前連接配接一直有效直到顯式修改為止。事務中執行的所有讀取操作也都會在指定的隔離級别規則下運作,除非在 SELECT 操作語句中對表指定了其它的鎖或者版本控制行為。
注:上面的時序圖隻是用來幫助了解事務的隔離級别,隻是一個大概的執行順序,當然也跟我執行事務 A 和 事務 B 的時間點相關,是以并不能真正反映實際過程中 SQL 語句送出和執行的實際順序,真正送出的過程可以通過 SQL Profiler 去跟蹤看看。