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判断是否已经处理,已经处理的,不再处理。
更多分享,请大家关注我的个人公众号: