事務是一組必須全部成功或者全部失敗的操作。事務的目標是保證數據總能處于有效一致的狀態。例如,轉賬操作。
事務有 4 個被稱為 ACID 屬性的特征,ACID 是以下概念的縮寫:
- Atomic(原子性):事務中所有步驟必須同時成功或失敗
- Consist(一致性):事務使底層數據庫在穩定狀態間轉換
- Isolated(隔離性):每個事務都是獨立的實體,一個事務不應該影響同時運行的其他事物。
- Durable(持久性):在事務成功前,事務產生的變化永久的存儲在媒質上,同時也必須維護日志以保證出現硬件故障數據庫也能得以恢復。
這些事事務的理想特征,它們未必總能達到。執行事務時 RDBMS 需要鎖定數據,這樣其他用戶就不能訪問它了。鎖越多,粒度就越大,執行事務時其他用戶就更不可能完成某些任務。也就是說,需要在用戶并發性和隔離性間做出權衡。
事務和 ASP.NET 應用程序
ASP.NET 可以使用 3 種基本類型的事務:
- 存儲過程事務:這些事務完全在數據庫中處理,存儲過程事務提供最佳的性能,因為只需往返一次數據庫。缺點是需要用 SQL 語句編寫事務處理邏輯。
- 客戶端引發(ADO.NET)的事務:這些事務由代碼通過編程來控制。它們使用和存儲過程事務一樣的命令,代碼使用了封裝這些細節的 ADO.NET 對象。缺點是在事務開始和提交時需要額外往返數據庫。
- COM+ 事務:COM+ 采用兩步提交協議,總會帶來額外開銷。只有當事務需要跨越多個資源管理器的時候才需要使用 COM+事務。一個COM+可以跨越 SQL Server 及 Oracle 數據庫中的交互。
盡管ADO.NET提供對事務的良好支持,但還是不應該隨便使用。每次使用都會為系統帶來額外的負擔。另外,事務會鎖定表的某些行,不必要的事務會損害你的應用程序性能。
使用事務應遵循這些實踐原則以獲得最佳效果:
- 事務要盡量短
- 不要在事務中間使用 SELECT 查詢返回數據
- 如果事務中確實要獲取記錄,應該只獲取確實需要的記錄,這樣可以減少鎖定的資源數
- 可能的情況下,在存儲過程中使用事務,而不是在 ADO.NET 中使用事務。這樣的事務可以被更快的啟動和編譯,因為數據庫服務器不需要與客戶端(Web 應用程序)交互。
- 避免使用具有多個獨立批處理任務的事務,把各個批處理任務作為單個事務
- 盡量避免影響大批量記錄的更新
1. 存儲過程事務
只要可能,事務存放的最佳地點是存儲過程代碼。它最有可能獲得最佳性能,所有的活動在數據源進行而不需要任何網絡通信。總之,事務的跨度越短,數據庫的并發性越好,被序列化的數據庫請求就越少。
下面這段偽代碼演示了如何在兩個賬戶間轉移資金,它是一個簡化的版本,允許賬戶村礦為負數:
CREATE Procedure TransferAmount
(
@Amount Money,
@ID_A int,
@ID_B int
)
as
begin transaction
update Accounts set balance = balance + @Amount where AccountID = @ID_A
if(@@error > 0)
GOTO Problem
update Accounts set balance = balance - @Amount where AccountID = @ID_B
if(@@error > 0)
GOTO Problem
-- 沒有錯誤
commit
return
-- 有錯誤
Problem:
rollback
raiserror('Could not update.',16,1)
當在 Transact-SQL 中使用 @@error 值時,必須在每步操作完成后立即檢查該值!!!因為 @@error 在一條 SQL 語句成功執行后自動被重設為 0 。
如果使用 SQL Server2005 或更新的版本,有一個更為現代的 try/catch 結構的優勢來實現上面的示例。(GOTO 語句畢竟非議頗多)
CREATE Procedure TransferAmount
(
@Amount Money,
@ID_A int,
@ID_B int
)
as
begin try
begin transaction
update Accounts set balance = balance + @Amount where AccountID = @ID_A
update Accounts set balance = balance - @Amount where AccountID = @ID_B
-- 出錯會進入 catch 塊,這里可以直接提交
commit
end try
begin catch
if(@@trancount > 0)
rollback
-- 記錄異常信息
declare @ErrMsg nvarchar(4000),@ErrSeverity int
select @ErrMsg = ERROR_MESSAGE(),@ErrSeverity = ERROR_SEVERITY()
raiserror(@ErrMsg,@ErrSeverity,1)
end catch
這個示例檢查 @@trancount 以確定是否有事務在進行中。(變量 @@trancount 計算當前連接中進行的事務數,begin transaction 語句使之加1,而 rollback 和 commit 使之減1 。)
為了防止錯誤被 catch 塊吞噬,使用了 raiserror 語句。 ADO.NET 把該消息封裝成 SqlException 對象,后者需要你在 .NET 中進行捕獲。
2. 客戶端引發的 ADO.NET 事務
大多數 ADO.NET 數據提供程序都提供對數據庫事務的支持。
Transaction 類有兩個關鍵方法:
下面這個示例演示向 Employees 表中插入兩條記錄:
protected void Page_Load(object sender, EventArgs e)
{
string connStr = WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
SqlConnection conn = new SqlConnection(connStr);
SqlCommand cmd1 = new SqlCommand("insert into Employees(LastName,FirstName) values('Joe','Tester')", conn);
SqlCommand cmd2 = new SqlCommand("insert into Employees(LastName,FirstName) values('Harry','Sulivan')", conn);
SqlTransaction tran = null;
try
{
// Open the connection and create the transaction
conn.Open();
tran = conn.BeginTransaction();
// Enlist two commands in the transaction
cmd1.Transaction = tran;
cmd2.Transaction = tran;
// Execute both commands
cmd1.ExecuteNonQuery();
cmd2.ExecuteNonQuery();
// Commit the transaction
tran.Commit();
}
catch
{
// In the case of error,rool back the transaction
tran.Rollback();
}
finally
{
conn.Close();
}
}
僅僅創建和提交事務是不夠的,一定要設置 Command.Transaction 屬性為 Transaction 對象從而把 Command 對象顯式的列入事務中,事務進行中執行一個不在當前事務中的命令,會產生錯誤。
事務的隔離級別
隔離級別決定了事務對其他事務影響的數據的敏感度。默認情況下,當兩個事務獨立運行時,在第一個事務結束前,第一個事務插入的數據對其他事務不可見。
隔離級別的概念和鎖的概念緊密相關,因為確定事務的隔離級別就是確定所需的鎖的類型。
- 共享鎖:是事務讀取數據庫中的數據時產生的鎖。當表,行,或某個范圍內有共享鎖時,其他事務就不可用修改相應的數據,但多個用戶可以使用共享鎖并發讀取數據。
- 獨占鎖:禁止多個事務同時修改數據。當事務更新數據且沒有其他事物已經鎖定數據的同時,會產生獨占鎖。當有獨占鎖時,其他用戶不能讀取或更新數據。
在 SQL Server 存儲過程中,使用 SET TRANSACTION ISOLATION LEVEL 命令來設置隔離級別。在 ADO.NET 中,可向 Connection.BeginTransaction()方法傳入 IsolationLevel 枚舉值,見下表:
ReadUncommitted | 無共享鎖,也不會有獨占鎖。會導致臟數據讀,但可以提升性能。 |
ReadCommitted | 數據被事務讀時會產生共享鎖,避免了臟數據讀,但數據在事務結束前可能已經被修改,這樣可能會產生非重復讀或虛幻行。(SQL Server 默認隔離級別) |
Snapshot | 保存一份事務正在訪問的數據的副本,因此一個事務不會看到其他事務所做的修改。這個隔離級別減少了堵塞,因為當其他事務正在讀取被快照隔離事務鎖鎖定的數據時,它可以從數據副本中讀取數據。該選項僅被 SQL Server 2005 支持,而且需要通過數據庫級別的設定才能夠啟用。 |
RepeatableRead | 查詢中涉及的所有數據均被加上共享鎖。這樣避免了他人修改數據,同時也避免了不可重復的讀,還是可能會出現虛幻行。 |
Serializable | 在所使用的數據上的一系列鎖禁止了其他用戶在該范圍內更新或插入行。它是唯一可以刪除虛幻行的隔離級別,但是給并發訪問帶來非常消極的影響,多用戶場景中很少使用。 |
相關的數據庫術語:
- 臟讀:讀取了其他尚未提交的事務中的數據,但該事務可能被回滾。
- 不可重復讀:如果允許不可重復讀,那么在同一個事務中執行多次查詢可能會得到不同的數據。這是因為事務在進行過程中只讀取數據并不能阻止其他用戶修改數據。為了防止不可重復讀,數據庫服務器需要鎖定事務讀取的行。
- 虛幻行:指在初始讀取中沒有出現但在相同事務內后續讀取時出現的行。事務進行過程中其他用戶插入了記錄,就可能出現幻行。為了防止幻行,事務進行過程中查詢數據庫時要根據 WHERE 子句使用一個范圍鎖。
這些現象究竟是無害的小缺陷還是潛在的錯誤取決于你的特定需求。大多數情況下,不可重復讀和幻行只是小問題,使用鎖阻止它們發生并發的代價有點太高了,不太值得。ReadCommitted 對于大多數事務都適用。看一下不同隔離級別的對比:
隔離級別 | 臟 讀 | 不可重復讀 | 虛幻數據 | 并發性 |
未提交讀(Read uncommitted) | 是 | 是 | 是 | 最佳 |
提交度(Read committed) | 否 | 是 | 是 | 好 |
快照(Snapshot) | 否 | 否 | 否 | 好 |
重復讀(Repeatable read) | 否 | 否 | 是 | 一般 |
序列化(Serializable) | 否 | 否 | 否 | 最差 |