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

本來打算寫有關 SSIS Package 中的事務控制過程的,可是發現不少基本的概念仍是須要有 SQL Server 事務和事務的隔離級別作基礎鋪墊。因此花了點時間,把 SQL Server 數據庫中的事務概念,ACID 原則,事務中常見的問題,問題形成的緣由和事務隔離級別等這些方面的知識好好的整理了一下。web

其實有關 SQL Server 中的事務,說實話由於內容太多, 話題太廣,稍微力度控制很差就超過了我目前知識能力範圍,就不是三言兩語可以講清楚的。因此但願你們可以指出其中總結的不足之處,對我來講多了提升的機會,更能夠幫助你們加深對事務的理解。數據庫


本文涉及到的知識點:安全

  • SQL Server 數據庫中事務的概念
  • ACID 原則 (加了一部份內容專門解釋原子性,提到了顯示事務以及 XACT_ABORT 機制來確保事務的原子性)
  • 列出事務中常見的問題以及緣由:髒讀,未提交讀,不可重複讀,幻讀 
  • SQL Server中 事務的隔離級別以及它們如何作到避免髒讀,未提交讀,不可重複讀和幻讀 (用代碼描述了這些問題,而且使用時間序來解釋產生的緣由)

SQL Server 數據庫中事務的概念併發

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

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


數據庫中事務的 ACID 原則測試

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

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

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

持久性(Durability):數據持久化,事務一旦對數據的操做完成並提交後,數據修改就已經完成,即便服務重啓這些數據也不會改變。相反,若是在事務的執行過程當中,系統服務崩潰或者重啓,那麼事務全部的操做就會被回滾,即回到事務操做以前的狀態。

我理解在極端斷電或者系統崩潰的狀況下,一個發生在事務未提交以前,數據庫應該記錄了這個事務的"ID"和部分已經在數據庫上更新的數據。供電恢復數據庫從新啓動以後,這時完成所有撤銷和回滾操做。若是在事務提交以後的斷電,有可能更改的結果沒有正常寫入磁盤持久化,可是有可能丟失的數據會經過事務日誌自動恢復並從新生成以寫入磁盤完成持久化。

原子性的進一步理解

關於原子性,有必要在這裏多補充一下,由於咱們描述的概念是指在事務中的原子性。一條 SQL 語句和多條 SQL 語句在處理原子性上是有一些區別的,下面演示了這些區別。

先運行這些代碼,建立一個很是簡單的測試表,這張表只簡單模擬了一個帳戶的 ID 和帳戶餘額。

USE BIWORK_SSIS GO

IF OBJECT_ID('dbo.Account') IS NOT NULL
DROP TABLE dbo.Account GO

CREATE TABLE dbo.Account ( ID INT PRIMARY KEY, AccountBalance MONEY CHECK(AccountBalance >= 0) )

單條 SQL 語句的原子性

插入一條測試語句,而後再查詢一下結果。

這裏提到了自動提交事務,這時 T-SQL 默認的事務方式,它是一種可以自動執行並可以自動回滾事務的處理方式。SQL Server 除了自動提交事務以外,還有顯示事務和隱式事務,暫時不在這篇文章中討論它們的區別了。

上面的兩個自動提交事務中,每個自動提交事務只包含一條 SQL 語句,不能再分,要麼成功,要麼失敗。

再好比,在一條 SQL 語句中插入多條數據時,其中一條數據是符合約束的。但由於另一條數據違反了檢查約束,這樣也會致使整個 Insert 語句失敗,所以沒有一條數據可以插入到數據表中。

多條 SQL 語句造成的一個總體的原子性

假設下面的這兩條 Insert 語句構成一個具有原子性特徵的邏輯單元,是一個總體須要造成一個事務,那麼應該如何處理。

INSERT INTO dbo.Account VALUES(1004,-1) INSERT INTO dbo.Account VALUES(1005,500)

很顯然若是直接這麼執行的話,1004 插入失敗,1005 能夠插入成功,這樣就是兩個不一樣的事務了。SQL Server 提供了兩種方式來確保這種包含多組 SQL 語句的邏輯塊具有原子性特徵。

方式一 - 使用顯示事務組合多條 SQL 語句構成一個總體以實現事務的原子性

第一種就是很是常見的顯示事務,經過顯示的使用 BEGIN TRANSACTION, COMMIT TRANSACTION 以及 ROLLBACK TRANSACTION 命令將一組 SQL 語句造成一個完整的事務來提交,提交要麼成功,要麼失敗。

-- 開始一個事務
BEGIN TRANSACTION

-- TRY CATCH 語句
BEGIN TRY -- 這一條會違反檢查約束,插入失敗
    INSERT INTO dbo.Account VALUES(1004,-1) -- 這一條會插入成功,但此時事務還未真正提交
    INSERT INTO dbo.Account VALUES(1005,500) END TRY BEGIN CATCH -- 發生錯誤,事務回滾
    IF @@TRANCOUNT > 0
        ROLLBACK TRANSACTION; END CATCH; -- 沒有進入 CATCH 塊,提交事務
IF @@TRANCOUNT > 0
    COMMIT TRANSACTION; GO

固然最終的結果就是事務回滾,一條數據都沒有插入到數據表中,因此失敗時就所有失敗,確保了事務的原子性。

方式二 - 經過設置  XACT_ABORT 爲 ON 來確保事務的原子性

先來看默認的設置,當  XACT_ABORT 爲 OFF 狀態的時候。

-- SET XACT_ABORT OFF - 默認的 SQL Server 設置
SET XACT_ABORT OFF
BEGIN TRANSACTION
 -- 這一條會違反檢查約束,插入失敗
    INSERT INTO dbo.Account VALUES(1004,-1) -- 這一條會插入成功
 INSERT INTO dbo.Account VALUES(1005,500) COMMIT TRANSACTION

當  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 中能夠經過這樣的語法來設置事務的隔離級別 (從低到高排列):

SET TRANSACTION ISOLATION LEVEL { READ UNCOMMITTED
    | READ COMMITTED
    | REPEATABLE READ
    | SNAPSHOT | SERIALIZABLE } [ ; ]

下面經過代碼示例來演示各個事務隔離級別的表現,運行下面 SQL 語句,插入一條測試語句。

TRUNCATE TABLE BIWORK_SSIS.dbo.Account GO

INSERT INTO BIWORK_SSIS.dbo.Account VALUES(1001,1000) SELECT * FROM BIWORK_SSIS.dbo.Account GO

Read Uncommitted (未提交讀)

隔離級別最低,容易產生的問題就是髒讀,由於能夠讀取其它事務修改了的可是沒有提交的數據。它的做用跟在事務中 SELECT 語句對象表上設置 (NOLOCK) 相同。

打開兩個查詢窗口,第一個窗口表示事務 A, 第二個窗口表示事務B。 事務A 保持默認的隔離級別,事務B 設置它們的隔離級別爲 READ UNCOMMITTED, 能夠經過 DBCC USEROPITIONS 查看更改後的結果。

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED 
DBCC USEROPTIONS

測試步驟:

先執行事務 A 的 SQL 代碼

BEGIN TRANSACTION

UPDATE BIWORK_SSIS.dbo.Account SET AccountBalance = 500 
WHERE ID  = 1001

WAITFOR DELAY '00:00:10'

ROLLBACK TRANSACTION

SELECT * FROM BIWORK_SSIS.dbo.Account WHERE ID = 1001

立刻接着再執行 事務 B 的 SQL 代碼 

-- 第1次查詢 發生在 A 事務未提交或者回滾以前
SELECT * FROM BIWORK_SSIS.dbo.Account WHERE ID = 1001

WAITFOR DELAY '00:00:10'

-- 第2次查詢 發生在 A 事務回滾以後
SELECT * FROM BIWORK_SSIS.dbo.Account WHERE ID = 1001

能夠看出,事務 B 對 ID = 1001 的這條數據進行了兩次讀取,可是很顯然第一次讀取的數據是髒數據。下面模擬了一下它們發生的時序,雖然不算嚴謹,可是能夠幫助理解髒讀產生的緣由。

還能夠把事務B 的隔離級別改回來成爲默認的  READ COMMITTED,而後運行完事務 A 以後立刻運行帶有 NOLOCK 的查詢,效果和上面描述的也是一致的。 一旦加上 NOLOCK,能夠認爲它的做用就等同於隔離級別爲 READ UNCOMMITTED。

SELECT * FROM BIWORK_SSIS.dbo.Account WITH(NOLOCK) WHERE ID = 1001

 

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 命令開啓這個支持

ALTER DATABASE BIWORK_SSIS SET ALLOW_SNAPSHOT_ISOLATION ON

而且在開始前先清空其它的 ID,只保留 ID = 1001 的這條記錄。

DELETE FROM BIWORK_SSIS.dbo.Account WHERE 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 去跟蹤看看。

相關文章
相關標籤/搜索