.NET 資料庫事務的各種玩法進化
阿新 • • 發佈:2020-08-24
事務是資料庫系統中的重要概念,本文講解作者從業 CRUD 十餘載的事務多種使用方式總結。
- 以下所有內容都是針對單機事務而言,不涉及分散式事務相關的東西!
- 關於事務原理的講解不針對具體的某個資料庫實現,所以某些地方可能和你的實踐經驗不符。
---
## 認識事務
為什麼需要資料庫事務?
轉賬是生活中常見的操作,比如從A賬戶轉賬100元到B賬號。站在使用者角度而言,這是一個邏輯上的單一操作,然而在資料庫系統中,至少會分成兩個步驟來完成:
1.將A賬戶的金額減少100元
2.將B賬戶的金額增加100元。
![](https://img2020.cnblogs.com/blog/31407/202008/31407-20200824071752184-765142530.png)
在這個過程中可能會出現以下問題:
1.轉賬操作的第一步執行成功,A賬戶上的錢減少了100元,但是第二步執行失敗或者未執行便發生系統崩潰,導致B賬戶並沒有相應增加100元。
2.轉賬操作剛完成就發生系統崩潰,系統重啟恢復時丟失了崩潰前的轉賬記錄。
3.同時又另一個使用者轉賬給B賬戶,由於同時對B賬戶進行操作,導致B賬戶金額出現異常。
為了便於解決這些問題,需要引入資料庫事務的概念。
> 以上內容引用自:https://www.cnblogs.com/takumicx/p/9998844.html
---
## 認識 ADO.NET
ADO.NET是.NET框架中的重要元件,主要用於完成C#應用程式訪問資料庫。
![](https://img2020.cnblogs.com/blog/31407/202008/31407-20200824072133302-544974950.png)
ADO.NET的組成:
System.Data.Common → 各種資料訪問類的基類和介面
System.Data.SqlClient → 對Sql Server進行操作的資料訪問類
a) SqlConnection → 資料庫聯結器
b) SqlCommand → 資料庫命名物件
d) SqlDataReader → 資料讀取器
f) SqlParameter → 為儲存過程定義引數
g) SqlTransaction → 資料庫事物
---
## 事務1:ADO.NET
最原始的事務使用方式,缺點:
- 程式碼又臭又長
- 邏輯難控制,一不小心就忘了提交或回滾,隨即而來的是資料庫鎖得不到釋放、或者連線池不夠用
- 跨方法傳遞 Tran 物件太麻煩
推薦:★☆☆☆☆
```c#
SqlConnection conn = new SqlConnection(connString);
SqlCommand cmd = new SqlCommand();
cmd.Connection = conn;
try
{
conn.Open();
cmd.Transaction = conn.BeginTransaction();//開啟事務
int result = 0;
foreach (string sql in sqlList)
{
cmd.CommandText = sql;
result += cmd.ExecuteNonQuery();
}
cmd.Transaction.Commit();//提交事務
return result;
}
catch (Exception ex)
{
//寫入日誌...
if (cmd.Transaction != null)
cmd.Transaction.Rollback();//回滾事務
throw new Exception("呼叫事務更新方法時出現異常:" + ex.Message);
}
finally
{
if (cmd.Transaction != null)
cmd.Transaction = null;//清除事務
conn.Close();
}
```
---
## 事務2:SqlHepler
原始 ADO.NET 事務程式碼又臭又長,是時候封裝一個 SqlHelper 來操作 ADO.NET 了。比如:
```c#
SqlHelper.ExecuteNonQuery(...);
SqlHelper.ExecuteScaler(...);
```
這樣封裝之後對單次命令執行確實方法不了少,用著用著又發現,事務怎麼處理?重截一個 IDbTransaction 引數傳入嗎?比如:
```c#
SqlHelper.ExecuteNonQuery(tran, ...);
SqlHelper.ExecuteScaler(tran, ...);
```
推薦:★☆☆☆☆
好像也還行,勉強能接受。
隨著在專案不斷的實踐,總有一天不能再忍受這種 tran 傳遞的方式,因為它太容易漏傳,特別是跨方法傳來傳去的時候,真的太難了。
---
## 事務3:利用執行緒id
在早期 .NET 還沒有非同步方法的時候,對事務2的缺陷進行了簡單封裝,避免事務 tran 物件傳來傳去的問題。
其原因是利用執行緒id,在事務開啟之時儲存到 staic Dictionary 之中,在 SqlHelper.ExecuteXxx 方法執行之前獲取當前執行緒的事務物件,執行命令。
這樣免去了事務傳遞的惡夢,最終呈現的事務程式碼如下:
```c#
SqlHelper.Transaction(() =>
{
SqlHelper.ExecuteNonQuery(...); //不再需要顯式傳遞 tran
SqlHelper.ExecuteScaler(...);
});
```
這種事務使用起來非常簡單,不需要考慮事務提交/釋放問題,被預設應用在了 FreeSql 中,缺點:不支援非同步。
推薦:★★★☆☆
同線程事務使用簡單,同時又產生了設計限制:
- 預設是提交,遇異常則回滾;
- 事務物件線上程掛載,每個執行緒只可開啟一個事務連線,巢狀使用的是同一個事務;
- 事務體內程式碼不可以切換執行緒,因此不可使用任何非同步方法,包括FreeSql提供的資料庫非同步方法(可以使用任何 Curd 同步方法);
---
## 事務4:工作單元
顯式將 ITransaction 物件傳來傳去,說直接點像少女沒穿衣服街上亂跑一樣,不安全。而且到時候想給少女帶點貨(狀態),一絲不掛沒穿衣服咋帶貨(沒口袋)。
這個時候對 ITransaction 做一層包裝就顯得有必要了,在IUnitOfWork 中可以定義更多的狀態屬性。
推薦:★★★★☆
定義 IUnitOfWork 介面如下:
```c#
public interface IUnitOfWork : IDisposable
{
IDbTransaction GetOrBeginTransaction(); //建立或獲取對應的 IDbTransaction
IsolationLevel? IsolationLevel { get; set; }
void Commit();
void Rollback();
}
```
---
## 事務5:AOP 事務
技術不斷在發展,先來一堆理論:
> 以下內容引用自:https://www.cnblogs.com/zhugenqiang/archive/2008/07/27/1252761.html
AOP(Aspect-Oriented Programming,面向方面程式設計),可以說是OOP(Object-Oriented Programing,面向物件程式設計)的補充和完善。OOP引入封裝、繼承和多型性等概念來建立一種物件層次結構,用以模擬公共行為的一個集合。當我們需要為分散的物件引入公共行為的時候,OOP則顯得無能為力。也就是說,OOP允許你定義從上到下的關係,但並不適合定義從左到右的關係。例如日誌功能。日誌程式碼往往水平地散佈在所有物件層次中,而與它所散佈到的物件的核心功能毫無關係。對於其他型別的程式碼,如安全性、異常處理和透明的持續性也是如此。這種散佈在各處的無關的程式碼被稱為橫切(cross-cutting)程式碼,在OOP設計中,它導致了大量程式碼的重複,而不利於各個模組的重用。
而AOP技術則恰恰相反,它利用一種稱為“橫切”的技術,剖解開封裝的物件內部,並將那些影響了多個類的公共行為封裝到一個可重用模組,並將其名為“Aspect”,即方面。所謂“方面”,簡單地說,就是將那些與業務無關,卻為業務模組所共同呼叫的邏輯或責任封裝起來,便於減少系統的重複程式碼,降低模組間的耦合度,並有利於未來的可操作性和可維護性。AOP代表的是一個橫向的關係,如果說“物件”是一個空心的圓柱體,其中封裝的是物件的屬性和行為;那麼面向方面程式設計的方法,就彷彿一把利刃,將這些空心圓柱體剖開,以獲得其內部的訊息。而剖開的切面,也就是所謂的“方面”了。然後它又以巧奪天功的妙手將這些剖開的切面復原,不留痕跡。
使用“橫切”技術,AOP把軟體系統分為兩個部分:核心關注點和橫切關注點。業務處理的主要流程是核心關注點,與之關係不大的部分是橫切關注點。橫切關注點的一個特點是,他們經常發生在核心關注點的多處,而各處都基本相似。比如許可權認證、日誌、事務處理。Aop 的作用在於分離系統中的各種關注點,將核心關注點和橫切關注點分離開來。正如Avanade公司的高階方案構架師Adam Magee所說,AOP的核心思想就是“將應用程式中的商業邏輯同對其提供支援的通用服務進行分離。”
實現AOP的技術,主要分為兩大類:一是採用動態代理技術,利用擷取訊息的方式,對該訊息進行裝飾,以取代原有物件行為的執行;二是採用靜態織入的方式,引入特定的語法建立“方面”,從而使得編譯器可以在編譯期間織入有關“方面”的程式碼。
最終呈現的使用程式碼如下:
```c#
[Transactional]
public void SaveOrder()
{
SqlHelper.ExecuteNonQuery(...);
SqlHelper.ExecuteScaler(...);
}
```
推薦:★★★★☆
利用 \[Transactional\] 特性標記 SaveOrder 開啟事務,他其實是執行類似這樣的操作:
```c#
public void SaveOrder()
{
var (var tran = SqlHelper.BeginTransaction())
{
try
{
SqlHelper.ExecuteNonQuery(tran, ...);
SqlHelper.ExecuteScaler(tran, ...);
tran.Commit();
}
catch
{
tran.Roolback();
throw;
}
}
}
```
解決了即不用顯著傳遞 tran 物件,也解決了非同步邏輯難控制的問題。
目前該事務方式在 Asp.NETCore 中應用比較廣泛,實現起來相當簡單,利用動態代理技術,替換 Ioc 中注入的內容,動態攔截 \[Transactional\] 特性標記的方法。
使用 Ioc 後就不能再使用 SqlHelper 技術了,此時應該使用 Repository。
組合技術:Ioc + Repository + UnitOfWork
瞭解原理比較重要,本節講得比較抽象,如果想深入瞭解原理,請參考 FreeSql 的使用實現程式碼如下:
自定義倉儲基類
```csharp
public class UnitOfWorkRepository : BaseRepository
{
public UnitOfWorkRepository(IFreeSql fsql, IUnitOfWork uow) : base(fsql, null, null)
{
this.UnitOfWork = uow;
}
}
public class UnitOfWorkRepository : BaseRepository
{
public UnitOfWorkRepository(IFreeSql fsql, IUnitOfWork uow) : base(fsql, null, null)
{
this.UnitOfWork = uow;
}
}
```
注入倉儲、單例 IFreeSql、AddScoped(IUnitOfWork)
```csharp
public static IServiceCollection AddFreeRepository(this IServiceCollection services, params Assembly[] assemblies)
{
services.AddScoped(typeof(IReadOnlyRepository<>), typeof(UnitOfWorkRepository<>));
services.AddScoped(typeof(IBasicRepository<>), typeof(UnitOfWorkRepository<>));
services.AddScoped(typeof(BaseRepository<>), typeof(UnitOfWorkRepository<>));
services.AddScoped(typeof(IReadOnlyRepository<,>), typeof(UnitOfWorkRepository<,>));
services.AddScoped(typeof(IBasicRepository<,>), typeof(UnitOfWorkRepository<,>));
services.AddScoped(typeof(BaseRepository<,>), typeof(UnitOfWorkRepository<,>));
if (assemblies?.Any() == true)
foreach (var asse in assemblies)
foreach (var repo in asse.GetTypes().Where(a => a.IsAbstract == false && typeof(UnitOfWorkRepository).IsAssignableFrom(a)))
services.AddScoped(repo);
return services;
}
```
---
## 事務6:UnitOfWorkManager
推薦:★★★★★
(事務5)宣告式事務管理在底層是建立在 AOP 的基礎之上的。其本質是對方法前後進行攔截,然後在目標方法開始之前建立或者加入一個事務,在執行完目標方法之後根據執行情況提交或者回滾事務。
宣告式事務最大的優點就是不需要通過程式設計的方式管理事務,這樣就不需要在業務邏輯程式碼中摻雜事務管理的程式碼,只需在配置檔案中做相關的事務規則宣告(或通過等價的基於標註的方式),便可以將事務規則應用到業務邏輯中。因為事務管理本身就是一個典型的橫切邏輯,正是 AOP 的用武之地。
通常情況下,筆者強烈建議在開發中使用宣告式事務,不僅因為其簡單,更主要是因為這樣使得純業務程式碼不被汙染,極大方便後期的程式碼維護。
和程式設計式事務相比,宣告式事務唯一不足地方是,後者的最細粒度只能作用到方法級別,無法做到像程式設計式事務那樣可以作用到程式碼塊級別。但是即便有這樣的需求,也存在很多變通的方法,比如,可以將需要進行事務管理的程式碼塊獨立為方法等等。
事務6 UnitOfWorkManager 參考隔壁強大的 java spring 事務管理機制,而事務5只能定義單一事務行為(比如不能巢狀),事務6 UnitOfWorkManager 實現的行為機制如下:
六種傳播方式(propagation),意味著跨方法的事務非常方便,並且支援同步非同步:
- Requierd:如果當前沒有事務,就新建一個事務,如果已存在一個事務中,加入到這個事務中,預設的選擇。
- Supports:支援當前事務,如果沒有當前事務,就以非事務方法執行。
- Mandatory:使用當前事務,如果沒有當前事務,就丟擲異常。
- NotSupported:以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
- Never:以非事務方式執行操作,如果當前事務存在則丟擲異常。
- Nested:以巢狀事務方式執行。
參考 FreeSql 的使用方式如下:
第一步:配置 Startup.cs 注入
```csharp
//Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton(fsql);
services.AddScoped();
services.AddFreeRepository(null, typeof(Startup).Assembly);
}
```
| UnitOfWorkManager 成員 | 說明 |
| -- | -- |
| IUnitOfWork Current | 返回當前的工作單元 |
| void Binding(repository) | 將倉儲的事務交給它管理 |
| IUnitOfWork Begin(propagation, isolationLevel) | 建立工作單元 |
第二步:定義事務特性
```csharp
[AttributeUsage(AttributeTargets.Method)]
public class TransactionalAttribute : Attribute
{
///
/// 事務傳播方式
///
public Propagation Propagation { get; set; } = Propagation.Requierd;
///
/// 事務隔離級別
///
public IsolationLevel? IsolationLevel { get; set; }
}
```
第三步:引入動態代理庫
在 Before 從容器中獲取 UnitOfWorkManager,呼叫它的 var uow = Begin(attr.Propagation, attr.IsolationLevel) 方法
在 After 呼叫 Before 中的 uow.Commit 或者 Rollback 方法,最後呼叫 uow.Dispose
第四步:在 Controller 或者 Service 或者 Repository 中使用事務特性
```csharp
public class SongService
{
BaseRepository _repoSong;
BaseRepository _repoDetail;
SongRepository _repoSong2;
public SongService(BaseRepository repoSong, BaseRepository repoDetail, SongRepository repoSong2)
{
_repoSong = repoSong;
_repoDetail = repoDetail;
_repoSong2 = repoSong2;
}
[Transactional]
public virtual void Test1()
{
//這裡 _repoSong、_repoDetail、_repoSong2 所有操作都是一個工作單元
this.Test2();
}
[Transactional(Propagation = Propagation.Nested)]
public virtual void Test2() //巢狀事務,新的(不使用 Test1 的事務)
{
//這裡 _repoSong、_repoDetail、_repoSong2 所有操作都是一個工作單元
}
}
```
問題:是不是進方法就開事務呢?
不一定是真實事務,有可能是虛的,就是一個假的 unitofwork(不帶事務)。
也有可能是延用上一次的事務。
也有可能是新開事務,具體要看傳播模式。
---
## 結束語
技術不斷的演變進步,從 1.0 -> 10.0 需要慢長的過程。
同時呼籲大家不要盲目使用微服務,演變的過程週期漫長對專案的風險太高。
早上五點半醒來,寫下本文對事務理解的一點總結。謝謝!!
以上各種事務機制在 FreeSql 中都有實現,FreeSql 是功能強大的物件關係對映技術(O/RM),支援 .NETCore 2.1+ 或 .NETFramework 4.0+ 或 Xamarin。支援 MySql/SqlServer/PostgreSQL/Oracle/Sqlite/達夢/人大金倉/神舟通用/Access;單元測試數量 5000+,以 MIT 開源協議託管於 github:[https://github.com/dotnetcore/FreeSql](https://github.com/dotnetcore/FreeSql)