CAP事件总线在NetCore中的应用

官方文档:

https://cap.dotnetcore.xyz/user-guide/zh/getting-started/quick-start/

在前面的文章中,我们介绍过 Abp自带的本地事件总线,但它有几点不足

1:缺乏失败重试机制,即若发布事件失败或者订阅事件处理失败,他没有重试机制,导致业务和数据异常。

2:缺乏对存储的支持,没有集成数据库,对事件进行存储,事件的处理都是基于内存。

下面我们介绍CAP事件总线这个组件工具,它很好的实现了以上两个问题,同时,也是微软官方应用中推荐的事件总线组件。

CAP官方文档:https://cap.dotnetcore.xyz/user-guide/zh/getting-started/quick-start/

因内容比较多,我们今天先介绍CAP的基本应用,基于内存存储的模式。

我们新建一个web mvc的项目CAP事件总线,项目环境基于.NET6.0

Nuget中我们需要引用3个包:
DotNetCore.CAP:CAP的核心包

DotNetCore.CAP.InMemoryStorage:CAP的内存存储包

Savorboard.CAP.InMemoryMessageQueue:CAP基于内存的对列包

下面2个因我们示例采用内存存储才需要,若我们使用数据库,则选择安装对应CAP的数据库组件

 

 

然后,在Program.cs中配置CAP服务,若不是.NET6.0,则在startup.cs中注入

var services = builder.Services;
services.AddCap(x =>
{
    x.UseInMemoryStorage();
    x.UseInMemoryMessageQueue();
    x.FailedRetryInterval = 20; //重试的间隔事件,默认是60s,官网上配置有详细说明
});

完成以上配置,我们就可以开始使用CAP来发布和订阅事件了

发布消息,主要使用ICapPublisher对象

我们在Home控制器上初始化ICapPublisher 来发送一个简单的消息

消息体包含了消息名称和消息内容,消息名称用于订阅识别,如果是接入RabbitMQ,则对应它的Queue的名称

using DotNetCore.CAP;
using Microsoft.AspNetCore.Mvc;

namespace CAP事件总线.Controllers
{
    public class HomeController : Controller
    {

        private readonly ICapPublisher  _capPublisher;

        public HomeController( ICapPublisher capPublisher)
        {
            _capPublisher = capPublisher;
        }

        public IActionResult Index()
        {
            //发送消息
            _capPublisher.Publish("test.message", "我发出消息啦"); 

            return View();
        }

      
    }
}

消息发送的方法支持多样化,我们看下有哪些发送方法

      /// <summary>
        /// 异步发布对象消息
        /// </summary>
        /// <param name="name">消息名称</param>
        /// <param name="contentObj">消息内容体,可以是实体对象,也可以是匿名对象</param>
        /// <param name="callbackName">回调的消息名称,指消息发布后,由订阅端处理,若订阅端处理成功并返回内容,默认会再发布一个消息名为callbackName的消息,可以接着订阅处理</param>
        /// <param name="cancellationToken">是否取消</param>
        Task PublishAsync<T>(string name, T? contentObj, string? callbackName = null, CancellationToken cancellationToken = default);

        /// <summary>
        /// 使用自定义头异步发布对象消息
        /// </summary>
        /// <typeparam name="T">实体对象</typeparam>
        /// <param name="name">消息内容体,可以是实体对象,也可以是匿名对象</param>
        /// <param name="contentObj">可序列化的消息体</param>
        /// <param name="headers">头信息</param>
        /// <param name="cancellationToken"></param>
        Task PublishAsync<T>(string name, T? contentObj, IDictionary<string, string?> headers, CancellationToken cancellationToken = default);

        /// <summary>
        /// Publish an object message.
        /// </summary>
        /// <param name="name">the topic name or exchange router key.</param>
        /// <param name="contentObj">message body content, that will be serialized. (can be null)</param>
        /// <param name="callbackName">callback subscriber name</param>
        void Publish<T>(string name, T? contentObj, string? callbackName = null);

        /// <summary>
        /// Publish an object message.
        /// </summary>
        /// <param name="name">the topic name or exchange router key.</param>
        /// <param name="contentObj">message body content, that will be serialized. (can be null)</param>
        /// <param name="headers">message additional headers.</param>
        void Publish<T>(string name, T? contentObj, IDictionary<string, string?> headers);

总结起来呢,就是以下几点

1:必须要有一个消息名称name

2:消息体只要是object类型的都可以,实体需要可序列化

3:支持消息订阅处理完后的回调

4:支持添加消息头

上面的示例中,我们是用最简单的方式,发送一个消息名为test.message的消息,内容就是一个字符串

下面我写一个订阅,订阅有两种写法均可,一种是直接使用控制器的,另一种就是在服务类中

我们先上控制器上的代码

using DotNetCore.CAP;
using Microsoft.AspNetCore.Mvc;

namespace CAP事件总线.Controllers
{
    public class ConsumerController : Controller
    {
        [NonAction]
        [CapSubscribe("test.message")]
        public void ReceiveMessage(string message)
        {
            Console.WriteLine("message  is:" + message);
        }
    }
}

所有的订阅都是采用 CapSubscribe+消息名的方式

NonAction:这个是MVC自带的特性,跟CAP无关。若将NonAction属性应用在Controller中的Action方法上,即使该Action方法是公共方法,也会告知ActionInvoker不要选取这个Action来执行。这个属性主要用来保护Controller中的特定公共方法不会发布到Web上。或是当功能尚未开发完成就要进行部署时,若暂时不想将此方法删除,也可以使用这个属性。 简单的说,就是通过url访问不了,避免对外暴露。

我们运行起来看下:

 

 

 从截图中,直观看到,消息订阅是执行成功了。

若订阅不是在控制器上写呢,而是在服务层方法呢,应该怎么订阅?为了验证这个,我们新建一个MessageHandler 消息处理类

using DotNetCore.CAP;
using Microsoft.AspNetCore.Mvc;

namespace CAP事件总线.Models
{
    public class MessageHandler: ICapSubscribe //消息的处理
    {
        [CapSubscribe("test.message")]
        public void ReceiveMessage(string message)
        {
            Console.WriteLine("在类方法中订阅 message  is:" + message);
        }

    }
}

然后我们需要注入这个类

在 programe.cs中增加 

services.AddTransient<MessageHandler>();//若是服务层已经注入,则不需要

1:类方法必须继承 ICapSubscribe 接口对象

2:类必须依赖注入到服务中

看下执行结果:

 

 

可以看到方法中的订阅成功了。

以上是一个简单的CAP基于内存存储的简单示例。

但是从运行结果来看,CAP貌似不支持 同一个消息名,多个订阅者。

如果我们要多个消费端,那也简单,加个组名即可

 

  [CapSubscribe("test.message","组名1")]

  [CapSubscribe("test.message","组名2")]
每个方法,订阅同一个消息名(路由),但是组名不同。
这样就可以多端消费了

下一步,我们继续验证CAP的重试机制

我们定义一个静态的 i 变量,初始化为0。每次执行订阅,都对 i+1。  别问我 每次执行为啥 i 不是重新初始化,因为是  静态变量哈,自己理解。

在订阅端,我们设置 当I<3时,都抛出异常,大于3

上代码

using DotNetCore.CAP;
using Microsoft.AspNetCore.Mvc;

namespace CAP事件总线.Models
{
    public class MessageHandler: ICapSubscribe //消息的处理
    {
        static int i = 0;
        [CapSubscribe("test.message")]
        public void ReceiveMessage(string message)
        {
            i++;
            if(i<3)
            {
                throw new Exception("i 小于3,抛出异常");
            }
            Console.WriteLine("在类方法中订阅 message  is:" + message);
        }


    }
}

看运行结果

 

 

 

 

通过验证,CAP确实有重试机制。 关于重试机制,CAP的配置项中有详细说明

FailedRetryInterval 配置项

默认值:60 秒

在消息发送的时候,如果发送失败,CAP将会对消息进行重试,此配置项用来配置每次重试的间隔时间。

在消息消费的过程中,如果消费失败,CAP将会对消息进行重试消费,此配置项用来配置每次重试的间隔时间。

重试 & 间隔

在默认情况下,重试将在发送和消费消息失败的 4分钟后 开始,这是为了避免设置消息状态延迟导致可能出现的问题。
发送和消费消息的过程中失败会立即重试 3 次,在 3 次以后将进入重试轮询,此时 FailedRetryInterval 配置才会生效。

 示例代码:https://gitee.com/fei686868/CAP-Test

以上就是今天分享的内容。

 

启动看板路径为: 域名/cap

 

经过后期的使用和测试,总结以下2个要点:

环境,走mysql数据库存储的方式

1:订阅只会在同一个应用程序里面执行,哪怕其他应用程序也有订阅相同消息名称的,不会执行。即各个应用程序自己发布,自己订阅。

2:CAP不保证绝对的幂等性,有时候会因为一些特殊情况,导致重复订阅,对应解决方案是,发送时,都增加一个消息id,用于识别。订阅时,处理完将id记录下来,用id判断是否已经处理,已经处理的,不再处理。

 

更多分享,请大家关注我的个人公众号:

 

 

posted @ 2022-09-23 10:46  黄明辉  阅读(1360)  评论(0编辑  收藏  举报