1. 程式人生 > >.NET 資料庫事務的各種玩法進化

.NET 資料庫事務的各種玩法進化

事務是資料庫系統中的重要概念,本文講解作者從業 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)