1. 程式人生 > >【SqlServer】解析SqlServer中的事務

【SqlServer】解析SqlServer中的事務

RoCE 完全 相同 error 模式設置 情況 完成 鎖定 bsp

在這篇Blog中,筆者將會解析闡述SqlServer中的事務,希望可以對你有所幫助。

1.事務是什麽

事務就是單個邏輯單元執行的一系列操作。事務都具有ACID特性:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和持久性(Durability)。

原子性(Atomicity):事務必須是原子工作單元;對於其數據修改,要麽全都執行,要麽全都不執行。

一致性(Consistency):事務在完成時,必須使所有的數據都保持一致狀態。 在相關數據庫中,所有規則都必須應用於事務的修改,以保持所有數據的完整性。 事務結束時,所有的內部數據結構都必須是正確的。

隔離性(Isolation):由並發事務所做的修改必須與任何其他並發事務所做的修改隔離。 事務識別數據時數據所處的狀態,要麽是另一並發事務修改它之前的狀態,要麽是第二個事務修改它之後的狀態,事務不會識別中間狀態的數據。 這稱為可串行性,因為它能夠重新裝載起始數據,並且重播一系列事務,以使數據結束時的狀態與原始事務執行的狀態相同。

持久性(Durability):完成完全持久的事務之後,它的影響將永久存在於系統中。 該修改即使出現系統故障也將一直保持。 SQL Server 2014和更高版本將啟用延遲的持久事務。 提交延遲的持久事務後,該事務日誌記錄將保留在磁盤上。

2.控制事務

應用程序主要通過指定事務啟動和結束的時間來控制事務。


啟動事務包括通過顯式、自動提交或隱式事務來啟動。

顯式啟動事務:

通過函數發出BEGIN TRANSACTION、COMMIT TRANSACTION、COMMIT WORK、ROLLBACK TRANSACTION 或 ROLLBACK WORK 語句明確定義事務的開始和結束。 當事務結束時,連接將返回到啟動顯式事務前所處的事務模式,或者是隱式模式,或者是自動提交模式。

自動提交事務:

自動提交模式是 SQL Server 數據庫引擎的默認事務管理模式。 每個SQL語句在完成時,都被提交或回滾。 如果一個語句成功地完成,則提交該語句;如果遇到錯誤,則回滾該語句。 只要沒有顯式事務或隱性事務覆蓋自動提交模式,與數據庫引擎實例的連接就以此默認模式操作。

隱式事務:

當連接以隱式事務模式進行操作時,數據庫引擎實例將在提交或回滾當前事務後自動啟動新事務。 無須描述事務的開始,只需提交或回滾每個事務。 隱性事務模式生成連續的事務鏈。 通過 API 函數或 SET IMPLICIT_TRANSACTIONS ON 語句,將隱性事務模式設置為打開。

結束事務可以通過COMMIT 或 ROLLBACK 語句,或者通過相應 API 函數來結束事務

COMMIT語句:

如果事務成功,則提交。 COMMIT 語句保證事務的所有修改在數據庫中都永久有效。 COMMIT 語句還釋放事務使用的資源(例如,鎖)。

ROLLBACK語句:

如果事務中出現錯誤,或用戶決定取消事務,則回滾該事務。 ROLLBACK 語句通過將數據返回到它在事務開始時所處的狀態,來取消事務中的所有修改。 ROLLBACK 還釋放事務占用的資源。

例如下面是使用事務的常見方式:

--啟動事物
begin tran
    begin try
        insert into TableTest(name,age) values(test,12);
    end try
    begin catch
        select    ERROR_NUMBER() error_number,
                ERROR_SEVERITY() error_severity,
                ERROR_STATE() error_state,
                ERROR_PROCEDURE() error_procedure,
                ERROR_LINE() error_line,
                ERROR_MESSAGE() error_message;
        if(@@trancount>0)
            rollback tran     --回滾事務
    end catch
if(@@trancount>0)
    commit tran --提交事務

3.數據並發訪問產生的影響

修改數據的用戶可能會影響到同時讀取或修改相同數據其他行。即用戶並發訪問同一資源時,可能產生產生如下影響:

丟失更新:

當兩個或多個事務選擇同一行,然後基於最初選定的值更新該行時,會發生丟失更新問題。 每個事務都不知道其他事務的存在。 最後的更新將覆蓋由其他事務所做的更新,這將導致數據丟失。

例如,兩個編輯人員制作了同一文檔的電子副本。 每個編輯人員獨立地更改其副本,然後保存更改後的副本,這樣就覆蓋了原始文檔。 最後保存其更改副本的編輯人員覆蓋另一個編輯人員所做的更改。 如果在一個編輯人員完成並提交事務之前,另一個編輯人員不能訪問同一文件,則可避免此問題。

未提交的依賴關系(臟讀)

當第二個事務選擇其他事務正在更新的行時,會發生未提交的依賴關系問題。 第二個事務正在讀取的數據還沒有提交並且可能由更新此行的事務所更改。

例如,一個編輯人員正在更改電子文檔。 在更改過程中,另一個編輯人員復制了該文檔(該副本包含到目前為止所做的全部更改)並將其分發給預期的用戶。 此後,第一個編輯人員認為目前所做的更改是錯誤的,於是刪除了所做的編輯並保存了文檔。 分發給用戶的文檔包含不再存在的編輯內容,並且這些編輯內容應視為從未存在過。 如果在第一個編輯人員保存最終更改並提交事務之前,任何人都不能讀取更改的文檔,則可以避免此問題。

不一致的分析(不可重復讀)

當第二個事務多次訪問同一行而且每次讀取不同的數據時,會發生不一致的分析問題。 不一致的分析與未提交的依賴關系類似,因為其他事務也是正在更改第二個事務正在讀取的數據。 但是,在不一致的分析中,第二個事務讀取的數據是由已進行了更改的事務提交的。 此外,不一致的分析涉及多次(兩次或更多)讀取同一行,而且每次信息都被其他事務更改,因此我們稱之為“不可重復讀”。

例如,編輯人員兩次讀取同一文檔,但在兩次讀取之間,作者重寫了該文檔。 當編輯人員第二次讀取文檔時,文檔已更改。 原始讀取不可重復。 如果在編輯人員完成最後一次讀取文檔之前,作者不能更改文檔,則可以避免此問題。

虛擬讀取

執行兩個相同的查詢但第二個查詢返回的行集合是不同的,此時就會發生虛擬讀取。

上面並發訪問產生的這些負面影響,在SQLServer中提供了一種機制來限制這些情況,該機制就是數據庫中的事務隔離級別。在介紹事務的隔離級別後,筆者會詳細闡述這四種負面影響。

4.事務的隔離級別

事務指定一個隔離級別,該隔離級別定義一個事務必須與其他事務所進行的資源或數據更改相隔離的程度。

數據庫引擎隔離級別

ISO 標準定義了下列隔離級別,SQL Server 數據庫引擎支持所有這些隔離級別:

隔離級別 定義
未提交讀(READ UNCOMMITTED) 隔離事務的最低級別,只能保證不讀取物理上損壞的數據。 在此級別上,允許臟讀,因此一個事務可能看見其他事務所做的尚未提交的更改。
已提交讀(READ COMMITTED) 允許事務讀取另一個事務以前讀取(未修改)的數據,而不必等待第一個事務完成。 數據庫引擎保留寫鎖(在所選數據上獲取)直到事務結束,但是一執行 SELECT 操作就釋放讀鎖。 這是數據庫引擎默認級別。
可重復讀(REPEATABLE READ) 數據庫引擎保留在所選數據上獲取的讀鎖和寫鎖,直到事務結束。 但是,因為不管理範圍鎖,可能發生虛擬讀取。
可序列化(SERIALIZABLE) 隔離事務的最高級別,事務之間完全隔離。 數據庫引擎保留在所選數據上獲取的讀鎖和寫鎖,在事務結束時釋放它們。 SELECT 操作使用分範圍的 WHERE 子句時獲取範圍鎖,主要為了避免虛擬讀取。


SqlServer默認的隔離級別是READ COMMITTED。可通過 SQL 腳本 SET TRANSACTION ISOLATION LEVEL 語句修改隔離級別。

下表顯示了不同隔離級別導致的並發副作用。

隔離級別 臟讀 不可重復讀 虛擬讀取
未提交的讀取(READ UNCOMMITTED)
已提交的讀取(READ COMMITTED)
可重復的讀取(REPEATABLE READ)
可序列化(SERIALIZABLE)


通過這個表可以觀察出,默認的隔離級別READ COMMITTED 不支持臟讀,但是支持不可重復讀和虛擬讀取。

筆者為了更好的闡述並發產生的負面影響,在下面的例子中筆者不使用系統默認的隔離級別,重新設置為 未提交的讀取(READ UNCOMMITTED)
臟讀:
在第一個客戶端,執行如下命令:

begin tran
insert into TableTest(name,age) values(jamy,21);
--延遲20秒後,提交事務
waitfor delay 0:0:20
commit tran

在第二個客戶端中,執行如下命令:

--設置事務隔離級別為 未提交的讀取
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;  
GO
begin tran
select * from TableTest;
commit tran

在執行第一個客戶端後,然後迅速執行第二個客戶端。會發現第二個客戶端讀取到第一個客戶端還未提交的數據。
技術分享圖片
第二個客戶端之所以讀取到第一個客戶端未提交的數據,這是因為它的隔離級別READ UNCOMMITTED,使得它可以讀取到其他事務中未提交的數據。

不可重復讀:
在第一個客戶端中,執行如下命令:

--設置事務隔離級別為 未提交的讀取
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;  
GO
begin tran
--第一次查詢
select * from TableTest where age=21;
waitfor delay 0:0:10
--第二次查詢
select * from TableTest where age=21;
commit tran

第二個客戶端中,執行如下命令:

begin tran
update TableTest set name=gosling where age=21;
waitfor delay 0:0:20
commit tran

在執行第一個客戶端後,再執行第二個客戶端。會發現第一個客戶端中,同一個事務內,相同的查詢條件卻得到不同的數據:
技術分享圖片

虛擬讀取:
第一個客戶端命令:

--設置事務隔離級別為 未提交的讀取
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;  
GO
begin tran
--第一次查詢,查詢表格的第一行
select top 1 * from TableTest;
waitfor delay 0:0:10
--第二次查詢,查詢表格的第一行
select top 1 * from TableTest;
commit tran

第二個客戶端的SQL語句如下:

begin tran
--設置影響的行數為1
set rowcount 1;
delete from TableTest;
--恢復默認
set rowcount 0;
waitfor delay 0:0:20
commit tran


在第一個客戶端執行後,立即執行第二個客戶端,在第一個客戶端的結果中可以看出,雖然執行相同的命令,但是兩次得到的結果是不同的。

上面讀者也需已經知道了設置隔離級別為未提交的讀取(READ UNCOMMITTED),其余的隔離級別推理也就可以知道。隔離級別通過鎖來控制並發產生的負面影響,下面介紹鎖。

5.鎖

鎖是 SQL Server 數據庫引擎用來同步多個用戶同時對同一個數據塊的訪問的一種機制。事務的隔離級別也是通過控制鎖的釋放時間來控制並發產生的負面影響。

應用程序一般不直接請求鎖。 鎖由數據庫引擎的一個部件(稱為“鎖管理器”)在內部管理。 當數據庫引擎實例處理 Transact-SQL 語句時,數據庫引擎查詢處理器會決定將要訪問哪些資源。 查詢處理器根據訪問類型和事務隔離級別設置來確定保護每一資源所需的鎖的類型。 然後,查詢處理器將向鎖管理器請求適當的鎖。 如果與其他事務所持有的鎖不會發生沖突,鎖管理器將授予該鎖。

鎖模式:

鎖模式 描述
共享 (S) 用於不更改或不更新數據的讀取操作,如 SELECT 語句。
更新 (U) 用於可更新的資源中。 防止當多個會話在讀取、鎖定以及隨後可能進行的資源更新時發生常見形式的死鎖。
排他 (X) 用於數據修改操作,例如 INSERT、UPDATE 或 DELETE。 確保不會同時對同一資源進行多重更新。

共享鎖(S 鎖)允許並發事務在封閉式並發控制下讀取 (SELECT) 資源。 資源上存在共享鎖(S 鎖)時,任何其他事務都不能修改數據。 讀取操作一完成,就立即釋放資源上的共享鎖(S 鎖),除非將事務隔離級別設置為可重復讀或更高級別,或者在事務持續時間內用鎖定提示保留共享鎖(S 鎖)。
更新鎖

更新鎖(U 鎖)可以防止常見的死鎖。 在可重復讀或可序列化事務中,此事務讀取數據 [獲取資源(頁或行)的共享鎖(S 鎖)],然後修改數據 [此操作要求鎖轉換為排他鎖(X 鎖)]。 如果兩個事務獲得了資源上的共享模式鎖,然後試圖同時更新數據,則一個事務嘗試將鎖轉換為排他鎖(X 鎖)。 共享模式到排他鎖的轉換必須等待一段時間,因為一個事務的排他鎖與其他事務的共享模式鎖不兼容;發生鎖等待。 第二個事務試圖獲取排他鎖(X 鎖)以進行更新。 由於兩個事務都要轉換為排他鎖(X 鎖),並且每個事務都等待另一個事務釋放共享模式鎖,因此發生死鎖。

若要避免這種潛在的死鎖問題,請使用更新鎖(U 鎖)。 一次只有一個事務可以獲得資源的更新鎖(U 鎖)。 如果事務修改資源,則更新鎖(U 鎖)轉換為排他鎖(X 鎖)。
排他鎖

排他鎖(X 鎖)可以防止並發事務對資源進行訪問。 使用排他鎖(X 鎖)時,任何其他事務都無法修改數據;僅在使用 NOLOCK 提示或未提交讀隔離級別時才會進行讀取操作。

數據修改語句(如 INSERT、UPDATE 和 DELETE)合並了修改和讀取操作。 語句在執行所需的修改操作之前首先執行讀取操作以獲取數據。 因此,數據修改語句通常請求共享鎖和排他鎖。 例如,UPDATE 語句可能根據與一個表的聯接修改另一個表中的行。 在此情況下,除了請求更新行上的排他鎖之外,UPDATE 語句還將請求在聯接表中讀取的行上的共享鎖。

對於更新鎖中說的,對同一個數據來說,一個事務中的排他鎖和其他事務中的共享鎖不相兼容,那麽這是為什麽?這裏筆者假設可以相互兼容的話,即一個事務中的排他鎖和其他事務中的共享鎖相互兼容的話,那麽會引起如下的問題:

對同一資源,在一個事務A中有select語句,select持有共享鎖並且未釋放,同時在另一個事務B中有update語句,update已經執行完查詢操作,準備釋放共享鎖請求排他鎖。那麽這時候,無論事務隔離級別有多高,事務B都可以控制成功請求排他鎖,在修改數據後,可能覺得數據不正確,又重新修改了,那麽這個時候事務A中同樣的查詢條件有可能讀取到兩種狀態的數據。這就造成了不可重復讀現象。雖然在上面的事務的隔離級別中,有些隔離級別是允許出現不可重復讀的現象(默認的隔離級別就允許)。但是這裏出現的不可重復讀現象,將不受隔離級別限制(因為無論什麽隔離級別,排他鎖的事務都可以修改共享鎖的事務正在訪問的數據)。也就是說,不能控制不可重復讀現象了。

5.1 NOLOCK、HOLDLOCK、UPDLOCK

在上面我們介紹了鎖的一些概念,接下來,講解一下如何加鎖。
NOLOCK:無鎖
HOLDLOCK:持有共享鎖,直到整個事務完成,應該在被鎖對象不需要時立即釋放,等於SERIALIZABLE事務隔離級別
UPDLOCK:更新鎖
例如:

select * from TableA with (HOLDLOCK);

5.2 死鎖分析

常見的死鎖形勢為:事務A持有TableA的排他鎖請求TableB的排他鎖,事務B持有TableB的排他鎖請求TableA的排他鎖。
比如:

--第一個事務
begin tran
update TableA set age=12 where name=abc;
waitfor delay 0:0:10
update TableB set age=14 where name=abc;
commit tran
--第二個事務
begin tran
update TableB set age=12 where name=abc;
waitfor delay 0:0:10
update TableA set age=14 where name=abc;
commit tran

這兩個事務同時執行,然後會出現死鎖現象。除了這種死鎖現象,還有另一種死鎖現象,就是上面介紹更新鎖(U)所時提到的,那種死鎖比較隱蔽。
例如:

update TableA set age=12 where name=abc;
update TableA set age=13 where name=abc;

這兩個語句放到不同的事務中同時執行,是有可能出現死鎖現象的,原因在上面共享鎖的講解中已經解釋了。

除了可以給查詢字段添加索引外,還可以使用UPDLOCK來避免出現死鎖:

update TableA with (UPDLOCK) set age=12 where name=abc;
update TableA with (UPDLOCK) set age=13 where name=abc;

上面的兩個update可能不太直觀展示死鎖,下面的例子比較好的展示了該類型的死鎖,看如下的兩個事務:
第一個事務:

begin tran a
    select * from TableTest with (holdlock);
--      select * from TableTest with (updlock,holdlock) 避免死鎖
    waitfor delay 0:0:5
    update TableTest set name=asdddd;
commit tran a

第二個事務:

begin tran b
select * from TableTest with (holdlock);
--select * from TableTest with (updlock,holdlock) 避免死鎖
update TableTest set name=dfs;
commit tran b

先執行第一個事務,再執行第二個事務就會出現死鎖現象。只要在查詢中加入updlock,就可避免死鎖。

在項目可能會用到如下命令的SQL語句:

if not exists (select ... from TableA ....)
insert into TableA ....
else
update TableA set....

如果有並發執行該sql的情況的話,那麽這種SQL就無疑是增加了死鎖出現了幾率,因為select ... from TableA ....延長了共享鎖占用的時間。這時候需要加入更新鎖,更新鎖可以加到select、update、insert語句中,但是這裏筆者建議加到select語句中。

參考文檔:SqlServer事務鎖定和行版本控制

【SqlServer】解析SqlServer中的事務