乘風破浪,遇見最佳跨平臺跨終端框架.Net Core/.Net生態 - 淺析ASP.NET Core集成事件發佈訂閱,通過CAP和RabbitMQ實現跨服務一致性

什麼是集成事件

集成事件(Integration Event)用於使領域狀態在多個微服務或外部系統中保持同步。這種功能是通過在微服務之外發布集成事件來實現的。

image

當一個事件被髮布到多個接收方微服務(被訂閱到集成事件的微服務之多)時,每個接收方微服務中的適當事件處理程序會處理該事件。

與領域事件的區別

領域事件(DomainEvent)是推送到領域事件(DomainEvent)調度程序的消息,可基於IoC容器或任何其他方法作爲內存中轉存進程實現(如Mediator)。

集成事件(Integration Event)是將已提交事務和更新傳播到其他子系統,無論它們是其他微服務、綁定上下文,還是外部應用程序。(集成事件是跨服務的,領域事件則不是)

工作原理

基於事件的通信時,當值得注意的事件發生時,微服務會發布事件,例如更新業務實體時。其他微服務訂閱這些事件。微服務收到事件時,可以更新其自己的業務實體,這可能會導致發佈更多事件。這是最終一致性概念的本質。

通常通過使用事件總線(EventBus)實現來執行此發佈/訂閱系統。最終一致事務由一系列分佈式操作組成。在每個操作中,微服務會更新業務實體,併發布可觸發下一個操作的事件。

集成事件是單個應用程序級別的,不建議跨應用使用同一個集成事件,這將導致事件來源混亂(微服務必須獨立)

事件總線

事件總線可實現發佈/訂閱式通信,無需組件之間相互顯式識別。

image

微服務A發佈到事件總線,這會分發到訂閱微服務B和C,發佈服務器無需知道訂閱服務器。

事件總線與觀察者模式和發佈-訂閱模式相關。

動手實踐

https://github.com/TaylorShi/HelloDomainDrivenDesign

集成事件的實現方式

  • 發佈-訂閱,通過EventBus
  • 觀察者模式,由觀察者將事件發送給關注事件的人

定義集成事件

在應用層下屬的IntegrationEvents中我們定義了兩個示例集成事件。

訂單創建集成事件OrderCreatedIntegrationEvent

/// <summary>
/// 訂單創建集成事件
/// </summary>
public class OrderCreatedIntegrationEvent
{
    /// <summary>
    /// 構造函數
    /// </summary>
    /// <param name="orderId"></param>
    public OrderCreatedIntegrationEvent(long orderId) => OrderId = orderId;

    /// <summary>
    /// 訂單Id
    /// </summary>
    public long OrderId { get; }
}

訂單支付成功集成事件OrderPaymentSucceededIntegrationEvent

/// <summary>
/// 訂單支付成功集成事件
/// </summary>
public class OrderPaymentSucceededIntegrationEvent
{
    /// <summary>
    /// 構造函數
    /// </summary>
    /// <param name="orderId"></param>
    public OrderPaymentSucceededIntegrationEvent(long orderId) => OrderId = orderId;

    /// <summary>
    /// 訂單Id
    /// </summary>
    public long OrderId { get; }
}

總結

  • 集成事件是跨服務的領域事件
  • 集成事件大部分場景由領域事件驅動觸發,也有個別場景比如說定時任務觸發的。
  • 集成事件是跨微服務來傳遞信息的,無法通過事務來處理集成事件(可藉助Cap這樣的框架來實現最終一致性)
  • 僅在必要的情況下定義和使用集成事件,一旦引入了集成事件,比如EventBus,在應用程序發佈新版本的時候,新舊版本的事件發佈和訂閱都會受到影響。

使用RabbitMQ來實現EventBus

什麼是RabbitMQ

https://www.rabbitmq.com

RabbitMQ是一套開源(MPL)的消息隊列服務軟件,是由LShift提供的一個Advanced Message Queuing Protocol(AMQP)的開源實現,由以高性能、健壯以及可伸縮性出名的Erlang寫成。

image

通過Docker準備MYSQL實例

https://hub.docker.com/_/rabbitmq

image

包括很多Tag,其中帶有management的Tag代表是包含Web控制檯程序的。

docker run -d --name rabbitmq --restart unless-stopped -p 5672:5672 -p 15672:15672 rabbitmq:3.11.1-management

設置默認賬號密碼

docker run -d --name rabbitmq --restart unless-stopped -p 5672:5672 -p 15672:15672 -e RABBITMQ_DEFAULT_USER=root -e RABBITMQ_DEFAULT_PASS=password rabbitmq:3.11.1-management

image

訪問RabbitMQ控制檯:http://localhost:15672

image

輸入前面設置的賬號密碼(默認賬號和密碼都是guest),進入控制檯

image

使用CAP來實現集成事件發送和訂閱

什麼是CAP框架

https://github.com/dotnetcore/CAP

image

CAP是一個基於.NET Standard的C#庫,它是一種處理分佈式事務的解決方案,同樣具有Event Bus的功能,它具有輕量級、易使用、高性能等特點

在我們構建SOA或者微服務系統的過程中,我們通常需要使用事件來對各個服務進行集成,在這過程中簡單的使用消息隊列並不能保證數據的最終一致性,CAP採用的是和當前數據庫集成的本地消息表的方案來解決在分佈式系統互相調用的各個環節可能出現的異常,它能夠保證任何情況下事件消息都是不會丟失的

你同樣可以把CAP當做Event Bus來使用,CAP提供了一種更加簡單的方式來實現事件消息的發佈和訂閱,在訂閱以及發佈的過程中,你不需要繼承或實現任何接口。

這是CAP集在ASP.NET Core微服務架構中的一個示意圖:

image

CAP框架實際上實現了一個叫發件箱(Outbox)的設計模式,在我們每個微服務,比如微服務A的數據庫A,在這個數據庫內部它建立了兩張表,一張叫Publish事件表和一張叫Receive事件表。這兩張表用來記錄微服務A發出的和接收的事件。

當我們發出事件時,我們會把事件的存儲的邏輯與我們業務邏輯的事務合併,在同一個事務裏提交,這意味着當我們業務邏輯提交成功時,我們的事件表裏面的事件是一定存在的,它是與我們的業務邏輯的事務是強綁定的。如果說我們的業務邏輯失敗了,事務回滾了,這條事件是不會出現在我們的事件表裏的,這樣子就可以做到我們要發送的事件一定是與業務邏輯是一致的。

接下來就是由組件來負責將事件表裏的事件全部都發送到EventBus,比如說RabbitMQ消息隊列裏面去,由接收方訂閱。

對於訂閱的事件的話,設計的模式也是同理,當我們的應用程序在消息隊列獲取到信息的時候,它就會將這些消息持久化到我們的數據庫的Reveive事件表裏,這樣我們就可以在本地進行事務的處理、失敗重試等操作。

CAP支持主流的消息隊列作爲傳輸器,你可以按需選擇下面的包進行安裝:

dotnet add package DotNetCore.CAP.Kafka
dotnet add package DotNetCore.CAP.RabbitMQ
dotnet add package DotNetCore.CAP.AzureServiceBus
dotnet add package DotNetCore.CAP.AmazonSQS
dotnet add package DotNetCore.CAP.NATS
dotnet add package DotNetCore.CAP.RedisStreams
dotnet add package DotNetCore.CAP.Pulsar

CAP提供了主流數據庫作爲存儲,你可以按需選擇下面的包進行安裝:

// 按需選擇安裝你正在使用的數據庫
dotnet add package DotNetCore.CAP.SqlServer
dotnet add package DotNetCore.CAP.MySql
dotnet add package DotNetCore.CAP.PostgreSql
dotnet add package DotNetCore.CAP.MongoDB

首先配置CAP到Startup.cs文件中

public void ConfigureServices(IServiceCollection services)
{
    ......

    services.AddDbContext<AppDbContext>();

    services.AddCap(x =>
    {
        //如果你使用的EF進行數據操作,你需要添加如下配置:
        x.UseEntityFramework<AppDbContext>();  //可選項,你不需要再次配置 x.UseSqlServer 了

        //如果你使用的ADO.NET,根據數據庫選擇進行配置:
        x.UseSqlServer("數據庫連接字符串");
        x.UseMySql("數據庫連接字符串");
        x.UsePostgreSql("數據庫連接字符串");

        //如果你使用的 MongoDB,你可以添加如下配置:
        x.UseMongoDB("ConnectionStrings");  //注意,僅支持MongoDB 4.0+集羣

        //CAP支持 RabbitMQ、Kafka、AzureServiceBus、AmazonSQS 等作爲MQ,根據使用選擇配置:
        x.UseRabbitMQ("ConnectionStrings");
        x.UseKafka("ConnectionStrings");
        x.UseAzureServiceBus("ConnectionStrings");
        x.UseAmazonSQS();
    });
}

Controller中注入ICapPublisher然後使用ICapPublisher進行消息發送


public class PublishController : Controller
{
    private readonly ICapPublisher _capBus;

    public PublishController(ICapPublisher capPublisher)
    {
        _capBus = capPublisher;
    }

    //不使用事務
    [Route("~/without/transaction")]
    public IActionResult WithoutTransaction()
    {
        _capBus.Publish("xxx.services.show.time", DateTime.Now);

        return Ok();
    }

    //Ado.Net 中使用事務,自動提交
    [Route("~/adonet/transaction")]
    public IActionResult AdonetWithTransaction()
    {
        using (var connection = new MySqlConnection(ConnectionString))
        {
            using (var transaction = connection.BeginTransaction(_capBus, autoCommit: true))
            {
                //業務代碼

                _capBus.Publish("xxx.services.show.time", DateTime.Now);
            }
        }
        return Ok();
    }

    //EntityFramework 中使用事務,自動提交
    [Route("~/ef/transaction")]
    public IActionResult EntityFrameworkWithTransaction([FromServices]AppDbContext dbContext)
    {
        using (var trans = dbContext.Database.BeginTransaction(_capBus, autoCommit: true))
        {
            //業務代碼

            _capBus.Publish("xxx.services.show.time", DateTime.Now);
        }
        return Ok();
    }
}

Action上添加CapSubscribeAttribute來訂閱相關消息

public class PublishController : Controller
{
    [CapSubscribe("xxx.services.show.time")]
    public void CheckReceivedMessage(DateTime datetime)
    {
        Console.WriteLine(datetime);
    }
}

如果你的訂閱方法沒有位於Controller中,則你訂閱的類需要繼承ICapSubscribe


namespace xxx.Service
{
    public interface ISubscriberService
    {
        void CheckReceivedMessage(DateTime datetime);
    }

    public class SubscriberService: ISubscriberService, ICapSubscribe
    {
        [CapSubscribe("xxx.services.show.time")]
        public void CheckReceivedMessage(DateTime datetime)
        {
        }
    }
}

然後在Startup.cs中的ConfigureServices()中注入你的ISubscriberService

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<ISubscriberService,SubscriberService>();

    services.AddCap(x=>{});
}

藉助CAP發送集成事件

依賴包

https://www.nuget.org/packages/DotNetCore.CAP

dotnet add package DotNetCore.CAP

image

接下來我們在訂單創建領域事件OrderCreatedDomainEventHandler的處理事件中,通過Cap組件來發送我們的訂單創建集成事件OrderCreatedIntegrationEvent

/// <summary>
/// 訂單創建領域事件處理方法
/// </summary>
public class OrderCreatedDomainEventHandler : IDomainEventHandler<OrderCreatedDomainEvent>
{
    /// <summary>
    /// Cap發佈者
    /// </summary>
    readonly ICapPublisher _capPublisher;

    /// <summary>
    /// 構造函數
    /// </summary>
    /// <param name="capPublisher"></param>
    public OrderCreatedDomainEventHandler(ICapPublisher capPublisher)
    {
        this._capPublisher = capPublisher;
    }

    /// <summary>
    /// 處理方法
    /// </summary>
    /// <param name="notification"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public async Task Handle(OrderCreatedDomainEvent notification, CancellationToken cancellationToken)
    {
        await _capPublisher.PublishAsync("OrderCreated", new OrderCreatedIntegrationEvent(notification.Order.Id));
    }
}

訂閱其它微服務發送的集成事件消息

定義訂閱服務類SubscriberService,它繼承自DotNetCore.CAP中的ICapSubscribe接口,這樣就可以標記它爲訂閱服務的對象。

/// <summary>
/// 訂閱服務
/// </summary>
public class SubscriberService : ISubscriberService, ICapSubscribe
{
    IMediator _mediator;

    /// <summary>
    /// 構造函數
    /// </summary>
    /// <param name="mediator"></param>
    public SubscriberService(IMediator mediator)
    {
        _mediator = mediator;
    }

    /// <summary>
    /// 訂閱訂單創建成功集成事件
    /// </summary>
    /// <param name="event"></param>
    [CapSubscribe("OrderCreated")]
    public void OrderCreated(OrderCreatedIntegrationEvent @event)
    {
        //Do SomeThing
    }

    /// <summary>
    /// 訂閱訂單支付成功集成事件
    /// </summary>
    /// <param name="event"></param>
    [CapSubscribe("OrderPaymentSucceeded")]
    public void OrderPaymentSucceeded(OrderPaymentSucceededIntegrationEvent @event)
    {
        //Do SomeThing
    }
}

在訂閱服務內部的方法通過標記CapSubscribe來定義對指定名稱的集成事件的訂閱接收。

將CAP添加到事務一起

依賴包

https://www.nuget.org/packages/DotNetCore.CAP.MySql

dotnet add package DotNetCore.CAP.MySql

image

在這個包中,有一個靜態擴展方法,可以在事務提交的時候將Cap帶上,修改EFContext的構造函數,引入ICapPublisher

/// <summary>
/// EFContext
/// </summary>
public class EFContext : DbContext, IUnitOfWork, ITransaction
{
    protected IMediator _mediator;
    protected ICapPublisher _capPublisher;
    public EFContext(DbContextOptions options, IMediator mediator, ICapPublisher capPublisher) : base(options)
    {
        _mediator = mediator;
        _capPublisher = capPublisher;
    }

同時在其下屬的開啓事務BeginTransactionAsync方法,將原來的EF自帶的BeginTransaction方法替換成DotNetCore.CAP.MySql新增的,同時引入Cap對象。

/// <summary>
/// 開啓事務
/// </summary>
/// <returns></returns>
public Task<IDbContextTransaction> BeginTransactionAsync()
{
    if (_currentTransaction != null) return null;

    _currentTransaction = Database.BeginTransaction(_capPublisher, autoCommit: true);
    return Task.FromResult(_currentTransaction);
}

注意這裏的Database.BeginTransaction已經是DotNetCore.CAP.MySql新增的的靜態擴展方法了。

public static class CapTransactionExtensions
{
    /// <summary>
    /// Start the CAP transaction
    /// </summary>
    /// <param name="database">The <see cref="DatabaseFacade" />.</param>
    /// <param name="publisher">The <see cref="ICapPublisher" />.</param>
    /// <param name="autoCommit">Whether the transaction is automatically committed when the message is published</param>
    /// <returns>The <see cref="IDbContextTransaction" /> of EF dbcontext transaction object.</returns>
    public static IDbContextTransaction BeginTransaction(this DatabaseFacade database,
        ICapPublisher publisher, bool autoCommit = false)
    {
        var trans = database.BeginTransaction();
        publisher.Transaction.Value = ActivatorUtilities.CreateInstance<MySqlCapTransaction>(publisher.ServiceProvider);
        var capTrans = publisher.Transaction.Value.Begin(trans, autoCommit);
        return new CapEFDbTransaction(capTrans);
    }

同時我們還需要配置CAP框架,才能使其生效,正常應該在Startup.csConfigureServices中添加它,但是我們定義在之前的服務容器擴展類ServiceCollectionExtensions中。

/// <summary>
/// 添加集成事件總線
/// </summary>
/// <param name="services"></param>
/// <param name="configuration"></param>
/// <returns></returns>
public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration)
{
    // 先將訂閱服務注入進來
    services.AddTransient<ISubscriberService, SubscriberService>();

    // 添加CAP相關的服務和配置
    services.AddCap(options =>
    {
        // 告訴框架我們是要針對DomainContext來實現我們的EventBus,EventBus和我們數據庫共享數據庫連接
        options.UseEntityFramework<DomainContext>();

        // 使用RabbitMQ來作爲EventBus的消息隊列的存儲
        options.UseRabbitMQ(options =>
        {
            configuration.GetSection("RabbitMQ").Bind(options);
        });
        //options.UseDashboard();
    });

    return services;
}

這裏需要留意UseEntityFramework<DomainContext>()這個指向,表示我們是要針對DomainContext來實現我們的EventBus,EventBus和我們數據庫共享數據庫連接。

同時這裏還需要配置UseRabbitMQ來作爲EventBus的消息隊列的存儲,這裏需要引入一個新包。

依賴包

https://www.nuget.org/packages/DotNetCore.CAP.RabbitMQ

dotnet add package DotNetCore.CAP.RabbitMQ

image

這裏如果要啓用CAP的面板,還需要另外一個包

依賴包

https://www.nuget.org/packages/DotNetCore.CAP.Dashboard

dotnet add package DotNetCore.CAP.Dashboard

這個可以根據需要啓用:options.UseDashboard()

UseRabbitMQ內部,我們看到這裏我們還引入了一個RabbitMQ的配置,我們去appsettings.json中添加它。

{
    "RabbitMQ": {
        "HostName": "localhost",
        "UserName": "root",
        "Password": "0gsieyVXF#hxH4RN",
        // 將RabbitMQ的空間區分爲不同的空間,可認爲是一個租戶,相同的值會被認爲屬於同一個RabbitMQ集羣
        "VirtualHost": "/",
        // 隊列需要訂閱的Exchange的名稱
        "ExchangeName": "tesla_order_queue"
    },
}

這裏HostName就是RabbitMQ的地址了,因爲是本地,這裏可以用localhost,賬號密碼就是之前創建RabbitMQ實例用到的賬號密碼,如果沒有設置,那就是guest,接下來VirtualHost代表了RabbitMQ的空間名稱,同一個空間名稱的RabbitMQ會被認爲屬於同一個集羣。ExchangeName是消息交換需要用到的Exchange名稱。

最後我們在Startup.csConfigureServices中添加前面的靜態擴展方法AddEventBus

public void ConfigureServices(IServiceCollection services)
{
    services.AddEventBus(Configuration);

演示CAP發送事件

爲了演示,我們在訂單創建領域事件處理OrderCreatedDomainEventHandler方法中通過Cap發佈一個訂單創建OrderCreated的事件。

/// <summary>
/// 訂單創建領域事件處理方法
/// </summary>
public class OrderCreatedDomainEventHandler : IDomainEventHandler<OrderCreatedDomainEvent>
{
    /// <summary>
    /// Cap發佈者
    /// </summary>
    readonly ICapPublisher _capPublisher;

    /// <summary>
    /// 構造函數
    /// </summary>
    /// <param name="capPublisher"></param>
    public OrderCreatedDomainEventHandler(ICapPublisher capPublisher)
    {
        this._capPublisher = capPublisher;
    }

    /// <summary>
    /// 處理方法
    /// </summary>
    /// <param name="notification"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public async Task Handle(OrderCreatedDomainEvent notification, CancellationToken cancellationToken)
    {
        await _capPublisher.PublishAsync("OrderCreated", new OrderCreatedIntegrationEvent(notification.Order.Id));
    }
}

然後在訂閱的地方SubscriberService.OrderCreated添加斷點。

image

在這個流程中,我們會創建一個訂單創建的領域事件,在訂單創建的領域事件裏面,又發送一個訂單創建的集成事件,最後在訂閱服務裏面訂閱了訂單創建的集成事件。

image

我們來看下運行效果。

image

首先啓動之後,我們從RabbitMQ面板看到了一個連接。

image

這個連接就是來自我們的服務。

我們有一個隊列,這個隊列就是我們剛纔訂閱的隊列

image

我們這個隊列定義了兩個RouteKey,一個是OrderCreated,一個是OrderPaymentSucceeded

image

可以看到它自動創建了我們前面在配置中設置的Exchange tesla_order_queue,並且它的類型是topic

image

image

我們在Swagger中觸發一下創建訂單的請求。

image

image
image
image
image

最終我們成功接收到了集成事件消息。

這時候我們可以同步觀察一下數據庫的情況,集成事件發送表cap.published

image

其中Content的內容格式是

{
    "Headers": {
        "cap-callback-name": null,
        "cap-msg-id": "1582391784400445440",
        "cap-msg-name": "OrderCreated",
        "cap-msg-type": "TeslaOrder.API.Application.IntegrationEvents.OrderCreatedIntegrationEvent",
        "cap-senttime": "2022/10/18 23:22:55 +08:00",
        "cap-corr-id": "1582391784400445440",
        "cap-corr-seq": "0"
    },
    "Value": {
        "OrderId": 3
    }
}

再看看集成事件接收表cap.received

image

其Content內容格式是

{
    "Headers": {
        "cap-callback-name": null,
        "cap-msg-id": "1582389173176340480",
        "cap-msg-name": "OrderCreated",
        "cap-msg-type": "TeslaOrder.API.Application.IntegrationEvents.OrderCreatedIntegrationEvent",
        "cap-senttime": "2022/10/18 23:12:32 +08:00",
        "cap-corr-id": "1582389173176340480",
        "cap-corr-seq": "0",
        "cap-msg-group": "cap.queue.teslaorder.api.v1"
    },
    "Value": {
        "OrderId": 1
    }
}

總結CAP實現原理

  • 事件表
  • 事務控制

將事件的存儲嵌入到業務邏輯的事務中去,保證業務與事件是要麼都能存儲成功,要麼都失敗。

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章