[Abp vNext 原始碼分析] - 4. 工作單元

myzony發表於2019-07-01

一、簡要說明

統一工作單元是一個比較重要的基礎設施元件,它負責管理整個業務流程當中涉及到的資料庫事務,一旦某個環節出現異常自動進行回滾處理。

在 ABP vNext 框架當中,工作單元被獨立出來作為一個單獨的模組(Volo.Abp.Uow)。你可以根據自己的需要,來決定是否使用統一工作單元。

二、原始碼分析

整個 Volo.Abp.Uow 專案的結構如下,從下圖還是可以看到我們的老朋友 IUnitOfWorkManagerIUnitOfWork ,不過也多了一些新東西。看一個模組的功能,首先從它的 Module 入手,我們先看一下 AbpUnitofWorkModule 裡面的實現。

[Abp vNext 原始碼分析] - 4. 工作單元

2.1 工作單元的初始模組

開啟 AbpUnitOfWorkModule 裡面的程式碼,發現還是有點失望,裡面就一個服務註冊完成事件。

public override void PreConfigureServices(ServiceConfigurationContext context)
{
    context.Services.OnRegistred(UnitOfWorkInterceptorRegistrar.RegisterIfNeeded);
}

這裡的結構和之前看的 審計日誌 模組類似,就是註冊攔截器的作用,沒有其他特別的操作。

2.1.1 攔截器註冊

繼續跟進程式碼,其實現是通過 UnitOfWorkHelper 來確定哪些型別應該整合 UnitOfWork 元件。

public static void RegisterIfNeeded(IOnServiceRegistredContext context)
{
    // 根據回撥傳入的 context 繫結的實現型別,判斷是否應該為該型別註冊 UnitOfWorkInterceptor 攔截器。
    if (UnitOfWorkHelper.IsUnitOfWorkType(context.ImplementationType.GetTypeInfo()))
    {
        context.Interceptors.TryAdd<UnitOfWorkInterceptor>();
    }
}

繼續分析 UnitOfWorkHelper 內部的程式碼,第一種情況則是實現型別 (implementationType) 或型別的任一方法標註了 UnitOfWork 特性的話,都會為其註冊工作單元攔截器。

第二種情況則是 ABP vNext 為我們提供了一個新的 IUnitOfWorkEnabled 標識介面。只要繼承了該介面的實現,都會被視為需要工作單元元件,會在系統啟動的時候,自動為它繫結攔截器。

public static bool IsUnitOfWorkType(TypeInfo implementationType)
{
    // 第一種方式,即判斷具體型別與其方法是否標註了 UnitOfWork 特性。
    if (HasUnitOfWorkAttribute(implementationType) || AnyMethodHasUnitOfWorkAttribute(implementationType))
    {
        return true;
    }

    // 第二種方式,即判斷具體型別是否繼承自 IUnitOfWorkEnabled 介面。
    if (typeof(IUnitOfWorkEnabled).GetTypeInfo().IsAssignableFrom(implementationType))
    {
        return true;
    }

    return false;
}

2.2 新的介面與抽象

在 ABP vNext 當中,將一些 職責 從原有的工作單元進行了 分離。抽象出了 IDatabaseApiISupportsRollbackITransactionApi 這三個介面,這三個介面分別提供了不同的功能和職責。

2.2.1 資料庫統一訪問介面

這裡以 IDatabaseApi 為例,它是提供了一個 資料庫提供者(Database Provider) 的抽象概念,在 ABP vNext 裡面,是將 EFCore 作為資料庫概念來進行抽象的。(因為後續 MongoDb 與 MemoryDb 與其同級)

你可以看作是 EF Core 的 Provider ,在 EF Core 裡面我們可以實現不同的 Provider ,來讓 EF Core 支援訪問不同的資料庫。

[Abp vNext 原始碼分析] - 4. 工作單元

[Abp vNext 原始碼分析] - 4. 工作單元

而 ABP vNext 這麼做的意圖就是提供一個統一的資料庫訪問 API,如何理解呢?這裡以 EFCoreDatabaseApi<TDbContext> 為例,你檢視它的實現會發現它繼承並實現了 ISupportsSavingChanges ,也就是說 EFCoreDatabaseApi<TDbContext> 支援 SaveChanges 操作來持久化資料更新與修改。

public class EfCoreDatabaseApi<TDbContext> : IDatabaseApi, ISupportsSavingChanges
    where TDbContext : IEfCoreDbContext
{
    public TDbContext DbContext { get; }

    public EfCoreDatabaseApi(TDbContext dbContext)
    {
        DbContext = dbContext;
    }
    
    public Task SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        return DbContext.SaveChangesAsync(cancellationToken);
    }

    public void SaveChanges()
    {
        DbContext.SaveChanges();
    }
}

也就是說 SaveChanges 這個操作,是 EFCore 這個 DatabaseApi 提供了一種特殊操作,是該型別資料庫的一種特殊介面。

如果針對於某些特殊的資料庫,例如 InfluxDb 等有一些特殊的 Api 操作時,就可以通過一個 DatabaseApi 型別進行處理。

2.2.2 資料庫事務介面

通過最開始的專案結構會發現一個 ITransactionApi 介面,這個介面只定義了一個 事務提交操作(Commit),並提供了非同步方法的定義。

public interface ITransactionApi : IDisposable
{
    void Commit();

    Task CommitAsync();
}

跳轉到其典型實現 EfCoreTransactionApi 當中,可以看到該型別還實現了 ISupportsRollback 介面。通過這個介面的名字,我們大概就知道它的作用,就是提供了回滾方法的定義。如果某個資料庫支援回滾操作,那麼就可以為其實現該介面。

其實這裡按照語義,你也可以將它放在 EfCoreDatabaseApi<TDbContext> 進行實現,因為回滾也是資料庫提供的 API 之一,只是在 ABP vNext 裡面又將其歸為事務介面進行處理了。

這裡就不再詳細贅述該型別的具體實現,後續會在單獨的 EF Core 章節進行說明。

2.3 工作單元的原理與實現

在 ABP vNext 框架當中的工作單元實現,與原來 ABP 框架有一些不一樣。

2.3.1 內部工作單元 (子工作單元)

首先說內部工作單元的定義,現在是有一個新的 ChildUnitOfWork 型別作為 子工作單元。子工作單元本身並不會產生實際的業務邏輯操作,基本所有邏輯都是呼叫 UnitOfWork 的方法。

internal class ChildUnitOfWork : IUnitOfWork
{
    public Guid Id => _parent.Id;

    public IUnitOfWorkOptions Options => _parent.Options;

    public IUnitOfWork Outer => _parent.Outer;

    public bool IsReserved => _parent.IsReserved;

    public bool IsDisposed => _parent.IsDisposed;

    public bool IsCompleted => _parent.IsCompleted;

    public string ReservationName => _parent.ReservationName;

    public event EventHandler<UnitOfWorkFailedEventArgs> Failed;
    public event EventHandler<UnitOfWorkEventArgs> Disposed;

    public IServiceProvider ServiceProvider => _parent.ServiceProvider;

    private readonly IUnitOfWork _parent;

    // 只有一個帶引數的建構函式,傳入的就是外部的工作單元(帶事務)。
    public ChildUnitOfWork([NotNull] IUnitOfWork parent)
    {
        Check.NotNull(parent, nameof(parent));

        _parent = parent;

        _parent.Failed += (sender, args) => { Failed.InvokeSafely(sender, args); };
        _parent.Disposed += (sender, args) => { Disposed.InvokeSafely(sender, args); };
    }

    // 下面所有 IUnitOfWork 的介面方法,都是呼叫傳入的 UnitOfWork 例項。
    public void SetOuter(IUnitOfWork outer)
    {
        _parent.SetOuter(outer);
    }

    public void Initialize(UnitOfWorkOptions options)
    {
        _parent.Initialize(options);
    }

    public void Reserve(string reservationName)
    {
        _parent.Reserve(reservationName);
    }

    public void SaveChanges()
    {
        _parent.SaveChanges();
    }

    public Task SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        return _parent.SaveChangesAsync(cancellationToken);
    }

    public void Complete()
    {

    }

    public Task CompleteAsync(CancellationToken cancellationToken = default)
    {
        return Task.CompletedTask;
    }

    public void Rollback()
    {
        _parent.Rollback();
    }

    public Task RollbackAsync(CancellationToken cancellationToken = default)
    {
        return _parent.RollbackAsync(cancellationToken);
    }

    public void OnCompleted(Func<Task> handler)
    {
        _parent.OnCompleted(handler);
    }

    public IDatabaseApi FindDatabaseApi(string key)
    {
        return _parent.FindDatabaseApi(key);
    }

    public void AddDatabaseApi(string key, IDatabaseApi api)
    {
        _parent.AddDatabaseApi(key, api);
    }

    public IDatabaseApi GetOrAddDatabaseApi(string key, Func<IDatabaseApi> factory)
    {
        return _parent.GetOrAddDatabaseApi(key, factory);
    }

    public ITransactionApi FindTransactionApi(string key)
    {
        return _parent.FindTransactionApi(key);
    }

    public void AddTransactionApi(string key, ITransactionApi api)
    {
        _parent.AddTransactionApi(key, api);
    }

    public ITransactionApi GetOrAddTransactionApi(string key, Func<ITransactionApi> factory)
    {
        return _parent.GetOrAddTransactionApi(key, factory);
    }

    public void Dispose()
    {

    }

    public override string ToString()
    {
        return $"[UnitOfWork {Id}]";
    }
}

雖然基本上所有方法的實現,都是呼叫的實際工作單元例項。但是有兩個方法 ChildUnitOfWork 是空實現的,那就是 Complete()Dispose() 方法。

這兩個方法一旦在內部工作單元呼叫了,就會導致 事務被提前提交,所以這裡是兩個空實現。

下面就是上述邏輯的虛擬碼:

using(var transactioinUow = uowMgr.Begin())
{
    // 業務邏輯 1 。
    using(var childUow1 = uowMgr.Begin())
    {
        // 業務邏輯 2。
        using(var childUow2 = uowMgr.Begin())
        {
            // 業務邏輯 3。
            childUow2.Complete();
        }
        
        childUow1.Complete();
    }
    transactioinUow.Complete();
}

以上結構一旦某個內部工作單元丟擲了異常,到會導致最外層帶事務的工作單元無法呼叫 Complete() 方法,也就能夠保證我們的 資料一致性

2.3.2 外部工作單元

首先我們檢視 UnitOfWork 型別和 IUnitOfWork 的定義和屬性,可以獲得以下資訊。

  1. 每個工作單元是瞬時物件,因為它繼承了 ITransientDependency 介面。

  2. 每個工作單元都會有一個 Guid 作為其唯一標識資訊。

  3. 每個工作單元擁有一個 IUnitOfWorkOptions 來說明它的配置資訊。

    這裡的配置資訊主要指一個工作單元在執行時的 超時時間是否包含一個事務,以及它的 事務隔離級別(如果是事務性的工作單元的話)。

  4. 每個工作單元儲存了 IDatabaseApiITransactionApi 的集合,並提供了訪問/儲存介面。

  5. 提供了兩個操作事件 FailedDisposed

    這兩個事件分別在工作單元執行失敗以及被釋放時(呼叫 Dispose() 方法)觸發,開發人員可以掛載這兩個事件提供自己的處理邏輯。

  6. 工作單元還提供了一個工作單元完成事件組。

    用於開發人員在工作單元完成時(呼叫Complete() 方法)掛載自己的處理事件,因為是 List<Func<Task>> 所以你可以指定多個,它們都會在呼叫 Complete() 方法之後執行,例如如下程式碼:

    using (var uow = _unitOfWorkManager.Begin())
    {
     uow.OnCompleted(async () => completed = true);
     uow.OnCompleted(async()=>Console.WriteLine("Hello ABP vNext"));
    
     uow.Complete();
    }

以上資訊是我們檢視了 UnitOfWork 的屬性與介面能夠直接得出的結論,接下來我會根據一個工作單元的生命週期來說明一遍工作單元的實現。

一個工作單元的的構造是通過工作單元管理器實現的(IUnitOfWorkManager),通過它的 Begin() 方法我們會獲得一個工作單元,至於這個工作單元是外部工作單元還是內部工作單元,取決於開發人員傳入的引數。

public IUnitOfWork Begin(UnitOfWorkOptions options, bool requiresNew = false)
{
    Check.NotNull(options, nameof(options));

    // 獲得當前的工作單元。
    var currentUow = Current;
    // 如果當前工作單元不為空,並且開發人員明確說明不需要構建新的工作單元時,建立內部工作單元。
    if (currentUow != null && !requiresNew)
    {
        return new ChildUnitOfWork(currentUow);
    }

    // 呼叫 CreateNewUnitOfWork() 方法建立新的外部工作單元。
    var unitOfWork = CreateNewUnitOfWork();
    // 使用工作單元配置初始化外部工作單元。
    unitOfWork.Initialize(options);

    return unitOfWork;
}

這裡需要注意的就是建立新的外部工作單元方法,它這裡就使用了 IoC 容器提供的 Scope 生命週期,並且在建立之後會將最外部的工作單元設定為最新建立的工作單元例項。

private IUnitOfWork CreateNewUnitOfWork()
{
    var scope = _serviceProvider.CreateScope();
    try
    {
        var outerUow = _ambientUnitOfWork.UnitOfWork;

        var unitOfWork = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();

        // 設定當前工作單元的外部工作單元。
        unitOfWork.SetOuter(outerUow);

        // 設定最外層的工作單元。
        _ambientUnitOfWork.SetUnitOfWork(unitOfWork);

        unitOfWork.Disposed += (sender, args) =>
        {
            _ambientUnitOfWork.SetUnitOfWork(outerUow);
            scope.Dispose();
        };

        return unitOfWork;
    }
    catch
    {
        scope.Dispose();
        throw;
    }
}

上述描述可能會有些抽象,結合下面這兩幅圖可能會幫助你的理解。

[Abp vNext 原始碼分析] - 4. 工作單元

[Abp vNext 原始碼分析] - 4. 工作單元

我們可以在任何地方注入 IAmbientUnitOfWork 來獲取當前活動的工作單元,關於 IAmbientUnitOfWorkIUnitOfWorkAccessor 的預設實現,都是使用的 AmbientUnitOfWork

在該型別的內部,通過 AsyncLocal<IUnitOfWork> 來確保在不同的 非同步上下文切換 過程中,其值是正確且統一的。

構造了一個外部工作單元之後,我們在倉儲等地方進行資料庫操作。操作完成之後,我們需要呼叫 Complete() 方法來說明我們的操作已經完成了。如果你沒有呼叫 Complete() 方法,那麼工作單元在被釋放的時候,就會產生異常,並觸發 Failed 事件。

public virtual void Dispose()
{
    if (IsDisposed)
    {
        return;
    }

    IsDisposed = true;

    DisposeTransactions();

    // 只有呼叫了 Complete()/CompleteAsync() 方法之後,IsCompleted 的值才為 True。
    if (!IsCompleted || _exception != null)
    {
        OnFailed();
    }

    OnDisposed();
}

所以,我們在手動使用工作單元管理器構造工作單元的時候,一定要注意呼叫 Complete() 方法。

既然 Complete() 方法這麼重要,它內部究竟做了什麼事情呢?下面我們就來看一下。

public virtual void Complete()
{
    // 是否已經進行了回滾操作,如果進行了回滾操作,則不提交工作單元。
    if (_isRolledback)
    {
        return;
    }

    // 防止多次呼叫 Complete 方法,原理就是看 _isCompleting 或者 IsCompleted 是不是已經為 True 了。
    PreventMultipleComplete();

    try
    {
        _isCompleting = true;
        SaveChanges();
        CommitTransactions();
        IsCompleted = true;
        // 資料儲存了,事務提交了,則說明工作單元已經完成了,遍歷完成事件集合,依次呼叫這些方法。
        OnCompleted();
    }
    catch (Exception ex)
    {
        // 一旦在持久化或者是提交事務時出現了異常,則往上層丟擲。
        _exception = ex;
        throw;
    }
}

public virtual void SaveChanges()
{
    // 遍歷集合,如果物件實現了 ISupportsSavingChanges 則呼叫相應的方法進行資料持久化。
    foreach (var databaseApi in _databaseApis.Values)
    {
        (databaseApi as ISupportsSavingChanges)?.SaveChanges();
    }
}

protected virtual void CommitTransactions()
{
    // 遍歷事務 API 提供者,呼叫提交事務方法。
    foreach (var transaction in _transactionApis.Values)
    {
        transaction.Commit();
    }
}

protected virtual void RollbackAll()
{
    // 回滾操作,還是從集合裡面判斷是否實現了 ISupportsRollback 介面,來呼叫具體的實現進行回滾。
    foreach (var databaseApi in _databaseApis.Values)
    {
        try
        {
            (databaseApi as ISupportsRollback)?.Rollback();
        }
        catch { }
    }

    foreach (var transactionApi in _transactionApis.Values)
    {
        try
        {
            (transactionApi as ISupportsRollback)?.Rollback();
        }
        catch { }
    }
}

這裡可以看到,ABP vNext 完全剝離了具體事務或者回滾的實現方法,都是移動到具體的模組進行實現的,也就是說在呼叫了 Complete() 方法之後,我們的事務就會被提交了。

本小節從建立、提交、釋放這三個生命週期講解了工作單元的原理和實現,關於具體的事務和回滾實現,我會放在下一篇文章進行說明,這裡就不再贅述了。

為什麼工作單元常常配合 using 語句塊 使用,就是因為在提交工作單元之後,就可以自動呼叫 Dispose() 方法,對工作單元的狀態進行校驗,而不需要我們手動處理。

using(var uowA = _uowMgr.Begion())
{
    uowA.Complete();
}

2.3.3 保留工作單元

在 ABP vNext 裡面,工作單元有了一個新的動作/屬性,叫做 是否保留(Is Reserved)。它的實現也比較簡單,指定了一個 ReservationName,然後設定 IsReservedtrue 就完成了整個動作。

那麼它的作用是什麼呢?這塊內容我會在工作單元管理器小節進行解釋。

2.4 工作單元管理器

工作單元管理器在工作單元的原理/實現裡面已經有過了解,工作單元管理器主要負責工作單元的建立。

這裡我再挑選一個工作單元模組的單元測試,來說明什麼叫做 保留工作單元

[Fact]
public async Task UnitOfWorkManager_Reservation_Test()
{
    _unitOfWorkManager.Current.ShouldBeNull();

    using (var uow1 = _unitOfWorkManager.Reserve("Reservation1"))
    {
        _unitOfWorkManager.Current.ShouldBeNull();

        using (var uow2 = _unitOfWorkManager.Begin())
        {
            // 此時 Current 值是 Uow2 的值。
            _unitOfWorkManager.Current.ShouldNotBeNull();
            _unitOfWorkManager.Current.Id.ShouldNotBe(uow1.Id);

            await uow2.CompleteAsync();
        }

        // 這個時候,因為 uow1 是保留工作單元,所以不會被獲取到,應該為 null。
        _unitOfWorkManager.Current.ShouldBeNull();

        // 呼叫了該方法,設定 uow1 的 IsReserved 屬性為 false。
        _unitOfWorkManager.BeginReserved("Reservation1");

        // 獲得到了值,並且誒它的 Id 是 uow1 的值。
        _unitOfWorkManager.Current.ShouldNotBeNull();
        _unitOfWorkManager.Current.Id.ShouldBe(uow1.Id);

        await uow1.CompleteAsync();
    }

    _unitOfWorkManager.Current.ShouldBeNull();
}

通過對程式碼的註釋和斷點除錯的結果,我們知道了通過 Reserved 建立的工作單元它的 IsReserved 屬性是 true,所以我們呼叫 IUnitOfWorkManager.Current 訪問的時候,會忽略掉保留工作單元,所以得到的值就是 null

但是通過呼叫 BeginReserved(string name) 方法,我們就可以將指定的工作單元置為 當前工作單元,這是因為呼叫了該方法之後,會重新呼叫工作單元的 Initialize() 方法,在該方法內部,又會將 IsReserved 設定為 false

public virtual void Initialize(UnitOfWorkOptions options)
{
    // ... 其他程式碼。
    // 注意這裡。
    IsReserved = false;
}

保留工作單元的用途主要是在某些特殊場合,在某些特定條件下不想暴露給 IUnitOfWorkManager.Current 時使用。

2.5 工作單元攔截器

如果我們每個地方都通過工作單元管理器來手動建立工作單元,那還是比較麻煩的。ABP vNext 通過攔截器,來為特定的型別(符合規則)自動建立工作單元。

關於攔截器的註冊已經在文章最開始說明了,這裡就不再贅述,我們直接來看攔截器的內部實現。其實在攔截器的內部,一樣是使用工作單元攔截器我來為我們建立工作單元的。只不過通過攔截器的方式,就能夠無感知/無侵入地為我們構造健壯的資料持久化機制。

public override void Intercept(IAbpMethodInvocation invocation)
{
    // 如果型別沒有標註 UnitOfWork 特性,或者沒有繼承 IUnitOfWorkEnabled 介面,則不建立工作單元。
    if (!UnitOfWorkHelper.IsUnitOfWorkMethod(invocation.Method, out var unitOfWorkAttribute))
    {
        invocation.Proceed();
        return;
    }

    // 通過工作單元管理器構造工作單元。
    using (var uow = _unitOfWorkManager.Begin(CreateOptions(invocation, unitOfWorkAttribute)))
    {
        invocation.Proceed();
        uow.Complete();
    }
}

關於在 ASP.NET Core MVC 的工作單元過濾器,在實現上與攔截器大同小異,後續講解 ASP.NET Core Mvc 時再著重說明。

三、總結

ABP vNext 框架通過統一工作單元為我們提供了健壯的資料庫訪問與持久化機制,使得開發人員在進行軟體開發時,只需要關注業務邏輯即可。不需要過多關注與資料庫等基礎設施的互動,這一切交由框架完成即可。

這裡多說一句,ABP vNext 本身就是面向 DDD 所設計的一套快速開發框架,包括值物件(ValueObject)這些領域驅動開發的特殊概念也被加入到框架實現當中。

微服務作為 DDD 的一個典型實現,DDD 為微服務的劃分提供理論支援。這裡為大家推薦《領域驅動設計:軟體核心複雜性應對之道》這本書,該書籍由領域驅動設計的提出者編寫。

看了之後發現在大型系統當中(博主之前做 ERP 的,吃過這個虧)很多時候都是憑感覺來寫,沒有一個具體的理論來支援軟體開發。最近拜讀了上述書籍之後,發現領域驅動設計(DDD)就是一套完整的方法論(當然 不是銀彈)。大家在學習並理解了領域驅動設計之後,使用 ABP vNext 框架進行大型系統開發就會更加得心應手。

四、後記

關於本系列文章的更新,因為最近自己在做 物聯網(Rust 語言學習、數位電路設計)相關的開發工作,所以 5 月到 6 月這段時間都沒怎麼去研究 ABP vNext。

最近在學習領域驅動設計的過程中,發現 ABP vNext 就是為 DDD 而生的,所以趁熱打鐵想將後續的 ABP vNext 文章一併更新,預計在 7 月內會把剩餘的文章補完(核心模組)。

五、點選我跳轉到文章目錄

相關文章