第十五节:CAP框架简介和基于CAP实现微服务的事件总线
一. CAP框架简介
1. 什么是事件总线?
事件总线(EventBus)是一种机制,它允许不同的组件彼此通信而不彼此了解。 组件可以将事件发送到Eventbus,而无需知道是谁来接听或有多少其他人来接听。组件也可以侦听Eventbus上的事件,而无需知道谁发送了事件。 这样,组件可以相互通信而无需相互依赖。同样,很容易替换一个组件,只要新组件了解正在发送和接收的事件,其他组件就永远不会知道.
使用事件总线的目的:将微服务系统各组件之间进行解耦。
2. CAP框架简介
CAP 是一个在分布式系统中(SOA,MicroService)实现事件总线(EventBus)和 最终一致性(分布式事务)的一个开源的 C# 库,她具有轻量级,高性能,易使用等特点。
CAP 以 NuGet 包的形式提供,对项目无任何入侵,你仍然可以以你喜爱的方式来构建分布式系统。
CAP 具有 Event Bus 的所有功能,并且CAP提供了更加简化的方式来处理EventBus中的发布/订阅。
CAP 具有消息持久化的功能,也就是当你的服务进行重启或者宕机时,她可以保证消息的可靠性。
CAP 实现了分布式事务中的最终一致性,你不用再去处理这些琐碎的细节。
CAP 提供了基于 Microsoft DI 的 API 服务,它可以和你的 ASP.NET Core 系统进行无缝结合,并且能够和你的业务代码集成支持强一致性的事务处理。
CAP 是开源免费的.
参考:
官网地址:https://cap.dotnetcore.xyz/user-guide/zh/getting-started/quick-start/
GitHub: https://github.com/dotnetcore/CAP/tree/master/samples
官方博客: https://www.cnblogs.com/savorboard/
3. 优势
相对于直接集成消息队列,异步消息传递最强大的优势之一是可靠性,系统的一个部分中的故障不会传播,也不会导致整个系统崩溃。 在 CAP 内部会将消息进行存储,以保证消息的可靠性,并配合重试等策略以达到各个服务之间的数据最终一致性。
相对于其他的 Service Bus 或者 Event Bus, CAP 拥有自己的特色,它不要求使用者发送消息或者处理消息的时候实现或者继承任何接口,拥有非常高的灵活性。我们一直坚信约定大于配置,所以CAP使用起来非常简单,对于新手非常友好,并且拥有轻量级。CAP 采用模块化设计,具有高度的可扩展性。你有许多选项可以选择,包括消息队列,存储,序列化方式等,系统的许多元素内容可以替换为自定义实现。
4. Cap框架支持的消息队列 和 存储介质
(1).消息队列有:RabbitMQ、Kafka、Azure Service Bus、Amazon SQS、In-Memory Queue.
(2).存储介质有:In-Memory、SQLServer、MySQL、PostgreSql、MongonDB
5. Cap框架结构图
剖析:客户端调用微服务1→在本地事务中执行相关业务+发送消息存储到publish表中 →通过CAP框架开启新线程→CAP框架把消息发送到MQ中→MQ主动通过CAP框架调用微服务2→微服务2接收到消息并且本地业务执行成功,反馈ACK消息确认→MQ标记/删除消息。
PS:上述流程是针对最终一致性的事务来写的,如果仅仅是为了实现事件总线,则第二步直接发送消息存储到publish表中即可,无需开启本地事务来保证原子性。
整套流程涉及到4个角色:发布者、消息队列、订阅者、存储器。(PS:发布者和订阅者各自对应一个存储器,当然可以在一个DB中,发布者对应published表,订阅者对应received表,通常情况下发布者和订阅者各自是一个服务,所以各自对应一个存储器)
注:下图实线是用户代码,虚线是CAP框架内部实线,还有这里是推模式,消息队列主动调用用户服务代码(默认推模式是单线程的,所以订阅者的方法不需要考虑并发问题的;可以配置多个线程,但多个线程则无法保证消费顺序了,且订阅者方法内可能存在并发问题了。)。
下图为官方GitHub中的图
PS:以上内容均参考官方文档和博客。
二. RabbitMq的安装和操作
1. 安装步骤
(1). 安装Erl运行环境
A. 去官网:https://www.erlang.org/downloads 下载,然后安装,安装过程,直接默认选项,下一步到底,安装完成即可。
PS:这里用的版本为:【23.1】,安装路径为 D:\Program Files\erl-23.1
B. 安装完成后需要配置环境变量,这里以win10的配置为例:先配置 ERLANG_HOME = D:\Program Files\erl-23.1,再把 %ERLANG_HOME%\bin 加到Path变量中,如下图:
C. 在命令行中输入erl,如下图,说明安装成功。
(2). 安装RabbitMQ的服务端。
A. 去官网 https://www.rabbitmq.com/download.html 进行下载,并且安装,安装过程如下图,一路下一步,直到安装完成。
PS:这里用的版本为:【3.8.9】,安装路径为 D:\Program Files\RabbitMQ Server
B. 安装完成后,查看windows服务已启动,如下图:
C.进入安装目录,找到sbin文件夹,这里我的是 D:\Program Files\RabbitMQ Server\rabbitmq_server-3.8.9\sbin ,在该路径下运行指令:【rabbitmq-plugins enable rabbitmq_management】,安装可视化插件。
PS:如果指令不好用,需要在前面加个 ./ 表示当前路径。
然后通过地址:http://127.0.0.1:15672 访问RabbitMq的后台管理系统,账号和密码都是guest。
截止到此,RabbitMq已经安装完成,并且已经以服务的方式启动了,大功告成!!!
2. 基本操作
(1). 启动/关闭RabbitMq服务(两种模式)
A. 服务的模式启动:(推荐)
services.msc 查看服务,有一个RabbitMQ服务,右键启动或关闭即可即可。或者通过下面指令启动关闭服务【net start rabbitmq】【net stop rabbitmq】(这两指令不需要配置全局环境变量)。
B. 指令的模式启动:
前提:下面指令都需要在 RabbitMQ Server/rabbitmq_server-3.8.3/sbin 目录下进行. (也可以配置一下环境变量就不需要在该目录下执行了,全局执行指令即可)
必须先把已经安装的RabbitMQ关闭 或者 删除。
在安装目录下的sbin目录下运行指令【rabbitmq-server】,服务则启动,这种模式关闭窗口,服务则停止,如下图:
在安装目录下的sbin目录下运行指令【rabbitmq-server -detached】,服务则在后台启动,关闭窗口,服务则不停止,需要运行【rabbitmqctl stop】来停止服务,如下图:
PS:如果指令不好用,需要在前面加个 ./ 表示当前路径,eg 【./rabbitmq-server】。
(2).查看RabbitMq状态
【rabbitmqctl status】
PS: 如果上述的这几个指令不想每次都到安装目录下执行,则可以配置一些全局环境
新增:RabbitMq_Home = E:\Program Files\RabbitMQ Server\rabbitmq_server-3.8.9
path中添加:%RabbitMq_Home%\sbin
测试:
三. CAP之EventBus实战-快速上手
(目标:快速实现单服务使用CAP框架----基于内存存储和内存消息队列)
1. 新建项目,配置命令启动
新建项目Publisher1,具有发布者和订阅者两种角色,配置通过指令设置端口启动,使用端口 9001。
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { // 添加命令行支持(写在上面的Main里也可以) new ConfigurationBuilder().AddCommandLine(args).Build(); webBuilder.UseStartup<Startup>(); });
2. 通过Nuget给该项目安装下面程序集
【DotNetCore.CAP 5.0.3】 :CAP的主程序集
【DotNetCore.CAP.InMemoryStorage 5.0.3】:内存存储
【Savorboard.CAP.InMemoryMessageQueue 3.1.1】:内存消息队列
3. 编写发布者和订阅者相关的方法
(1).发布者方法:SendMsg, 先需要通过构造函数注入ICapPublisher _capBus,然后方法内部调用_capBus.Publish("ypfkey2", DateTime.Now);进行消息发送。
代码如下:
[Route("api/[controller]/[action]")] [ApiController] public class SimpleUseController : ControllerBase { private ICapPublisher _capBus; private ILogger _log; public SimpleUseController(ICapPublisher capBus, ILogger<SimpleUseController> log) { this._capBus = capBus; this._log = log; } /// <summary> /// 发布者调用的方法1 /// </summary> /// <returns></returns> public IActionResult SendMsg() { var nowTime = DateTime.Now; _capBus.Publish("ypfkey2", nowTime); _log.LogInformation($"我是发布者,发布的内容为:{nowTime}"); return Content("发送成功"); } }
注意:Publish的第一个参数是一个key,就是通过这个key来和订阅者建立起联系;第二个参数可以是任意类型.
(2).订阅者方法:ReceiveMsg,通过[CapSubscribe("ypfkey2")]来和发布者建立起联系,同时加一个[NonAction]特性,排除Controller中的调用。 (这里是基于内存存储,所以发布者和订阅者必须在一个项目中)
代码如下:
[Route("api/[controller]/[action]")] [ApiController] public class SimpleUseController : ControllerBase { private ICapPublisher _capBus; private ILogger _log; public SimpleUseController(ICapPublisher capBus, ILogger<SimpleUseController> log) { this._capBus = capBus; this._log = log; } /// <summary> /// 订阅者的方法 /// </summary> /// <param name="time"></param> [NonAction] [CapSubscribe("ypfkey2")] public void ReceiveMsg(DateTime time) { //1. 正常接收 _log.LogInformation($"我是订阅者,收到的内容为:{time}"); } }
注意:这里是‘推模式’,消息队列 主动发送请求 调用订阅者的方法, 二者通过ypfkey2这个标记来通信。
5. 在ConfigureService中添加代码如下代码进行注册
代码如下:
services.AddCap(x => { x.UseInMemoryStorage(); //内存存储 x.UseInMemoryMessageQueue(); //内存消息队列 });
6.启动项目,并测试
启动指令:【dotnet Publisher1.dll --urls="http://*:9001" --ip="127.0.0.1" --port=9001】
PostMan发送Get请求 http://localhost:9001/api/SimpleUse/SendMsg , 在控制台中先看到发布者的方法中的内容 ,然后看到订阅者方法的内容,说明搭建成功。
如图:
四. CAP之EventBus实战-深入探讨
(目标:多服务使用CAP框架---基于SQLServer/MySQL存储 和 RabbitMq消息队列)
1. 新建发布者和订阅者服务,并配置命令启动
新建发布者服务:Publisher1 订阅者服务:Subscriber1,这两项目均为.Net 5.0,配置通过指令设置端口启动,分别使用端口 9001和 9011。
代码如下:
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { // 添加命令行支持(写在上面的Main里也可以) new ConfigurationBuilder().AddCommandLine(args).Build(); webBuilder.UseStartup<Startup>(); });
启动指令如下:
【dotnet Publisher1.dll --urls="http://*:9001" --ip="127.0.0.1" --port=9001】
【dotnet Subscriber1.dll --urls="http://*:9011" --ip="127.0.0.1" --port=9011】
PS:这里的发布者和订阅者项目是两个微服务,属于不同的项目,所以不能使用内存消息队列,不通的。
2. 通过Nuget给两个项目安装程序集
【DotNetCore.CAP 5.0.3】:CAP主框架
【DotNetCore.CAP.RabbitMQ 5.0.3】:RabbitMq消息队列
【DotNetCore.CAP.SqlServer 5.0.3】:SQLServer存储
【DotNetCore.CAP.MySql 5.0.3】:MySQL存储
注:SQLServer和MySQL程序集不要同时安装!!!! 容易命名相同引用混乱。
3. 编写发布者方法
Publisher1发布者项目中准备SendMsg方法,注入ICapPublisher _capBus,调用_capBus.Publish("ypfkey1", DateTime.Now);进行消息发送。
注意:Publish的第一个参数是一个key,就是通过这个key来和订阅者建立起联系;第二个参数可以是任意类型.
代码如下:
[Route("api/[controller]/[action]")] [ApiController] public class PubController : ControllerBase { private ICapPublisher _capBus; private ILogger _log;
public PubController(ICapPublisher capBus, ILogger<PubController> log) { this._capBus = capBus; this._log = log; } /// <summary> /// 发布者调用的方法1(无事务) /// </summary> /// <returns></returns> public IActionResult SendMsg() { var nowTime = DateTime.Now; _capBus.Publish("ypfkey1", nowTime); _log.LogInformation($"我是发布者,发布的内容为:{nowTime}"); return Content("发送成功"); } }
4. 编写订阅者方法
SubSctiber1订阅者项目中准备ReceiveMsg方法, 通过[CapSubscribe("ypfkey1")]来和发布者建立起联系,同时加一个[NonAction]特性,排除Controller中的调用.
注意:这里是‘推模式’,消息队列 主动发送请求 调用订阅者的方法。
代码如下:
[Route("api/[controller]/[action]")] [ApiController] public class SubController : ControllerBase { private ILogger _log; public SubController(ILogger<SubController> log) { this._log = log; } /// <summary> /// 订阅者的方法 /// </summary> /// <param name="time"></param> [NonAction] [CapSubscribe("ypfkey1")] public void ReceiveMsg(DateTime time) { //1. 正常接收 { _log.LogInformation($"我是订阅者,收到的内容为:{time}"); } } }
5. 使用RabbitMQ作为消息队列
(1). 启动RabbitMq服务 (见上面的二)
(2). 两个项目都在ConfigureService注册RabbitMq服务
代码如下:
//注册cap事件 services.AddCap(x => { //-----------------------------一.声明存储类型---------------------------------
//1. 使用xxx存储 //-----------------------------二.声明消息队列类型---------------------------------
//1.使用RabbitMq队列存储 x.UseRabbitMQ(rb => { rb.HostName = "localhost"; rb.UserName = "guest"; rb.Password = "guest"; rb.Port = 5672; rb.VirtualHost = "/"; //rb.QueueMessageExpires = 24 * 3600 * 10; //队列中消息自动删除时间(默认10天) }); });
6. 使用SQLServer作为消息存储
前提:先删掉【DotNetCore.CAP.MySql】,否则存在调用二义性。
(1). 通过Nuget安装【DotNetCore.CAP.SqlServer 5.0.3】
(2).安装EFCore-SQLServer相关的程序集
【Microsoft.EntityFrameworkCore 5.0.6】【Microsoft.EntityFrameworkCore.SqlServer 5.0.6】
【Microsoft.EntityFrameworkCore.Tools 5.0.6】【Microsoft.EntityFrameworkCore.Design 5.0.6】
(3). 在SQLServer中新建两个空的数据库分别是PubDB和SubDB,分别服务于发布者和订阅者
A.在Publisher1项目中执行下面映射指令:
【Scaffold-DbContext "Server=localhost;Database=PubDB;User ID=sa;Password=123456;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -UseDatabaseNames -DataAnnotations -NoPluralize】
B.在Subscriber1项目中执行下面映射指令:
【Scaffold-DbContext "Server=localhost;Database=SubDB;User ID=sa;Password=123456;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -UseDatabaseNames -DataAnnotations -NoPluralize】
(4).两个项目都修改ConfigureService代码
A. EFCore自身注入:
services.AddDbContext<PubDBContext>(option => option.UseSqlServer(Configuration.GetConnectionString("EFStr")));
B. Cap框架依赖SQLServer,(下面选择一种即可)
① x.UseEntityFramework<PubDBContext>(); //使用EFCore
② x.UseSqlServer(Configuration.GetConnectionString("EFStr")); //使用ADO.NET (不需要依赖EF上下文,没有数据库的话,事先建好数据库即可)
发布者代码:(订阅者代码类似,上下文改为 SubDbContext)
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); //数据库连接(SQLServer) services.AddDbContext<PubDBContext>(option => option.UseSqlServer(Configuration.GetConnectionString("EFStr")));//注册cap事件 services.AddCap(x => { //-----------------------------一.声明存储类型---------------------------------
//1. 使用SQLServer存储 //还需要配合上面EF上下文的注入 services.AddDbContext x.UseEntityFramework<PubDBContext>(); //EFCore配置 //x.UseSqlServer(Configuration.GetConnectionString("EFStr")); //ADO.Net配置
//-----------------------------二.声明消息队列类型---------------------------------//1.使用RabbitMq队列存储 x.UseRabbitMQ(rb => { rb.HostName = "localhost"; rb.UserName = "guest"; rb.Password = "guest"; rb.Port = 5672; rb.VirtualHost = "/"; //rb.QueueMessageExpires = 24 * 3600 * 10; //队列中消息自动删除时间(默认10天) }); //-----------------------------三.添加后台监控,用于人工干预---------------------------------//-----------------------------四.通用配置--------------------------------- }); }
发布者连接字符串:(订阅者代码类似,数据库改为:SubDB)
"ConnectionStrings": {
"EFStr": "Server=localhost;Database=PubDB;User ID=sa;Password=123456;",
}
(5). 启动项目并测试
启动指令:【dotnet Publisher1.dll --urls="http://*:9001" --ip="127.0.0.1" --port=9001】 【dotnet Subscriber1.dll --urls="http://*:9011" --ip="127.0.0.1" --port=9011】
然后用PostMan发送Get请求 http://localhost:9001/api/Pub/SendMsg ,订阅者收到发布者发送的消息,如下图:测试通过。
另外,PubDB和SubDB都各自生成两张表,因为现在发布者和订阅者是两个服务,所有发布者只使用PubDB数据库中的cap.Published表,订阅者只使用SubDB数据库中的cap.Received表。
A.cap.Published (Publisher1中只用该表)
B.cap.Received (Subscriber1中只用该表)
(6).分析表结构:
Published表:Name为标记key,Content为发送的内容,Retries为重试次数,ExpriesAt过期时间、StatusName状态
Received表:含义同上,多了个Group组
特别注意:CAP框架发消息给订阅者默认是单线程执行的,上一个线程执行结束下一个线程才能开始,所以订阅者中不需要考虑并发问题,Received表中数据默认状态为Schedued,执行完成,才会变为Succeed或Failed。
7.使用MySQL作为消息存储
前提:先删掉【DotNetCore.CAP.SqlServer】,否则存在调用二义性
(1).通过Nuget安装【DotNetCore.CAP.MySql 5.0.3】
(2).安装EFCore-MySQL相关程序集
【Microsoft.EntityFrameworkCore 5.0.6】【Microsoft.EntityFrameworkCore.Design 5.0.6】
【Microsoft.EntityFrameworkCore.Tools 5.0.6】【Pomelo.EntityFrameworkCore.MySql】
(3).在MySQL数据库中新建两个空的数据库分别是PubDB和SubDB,
A.在Publisher1项目中执行下面映射指令:
【Scaffold-DbContext "Server=localhost;Database=PubDB;User ID=root;Password=123456;" Pomelo.EntityFrameworkCore.MySql -OutputDir Models -UseDatabaseNames -DataAnnotations -NoPluralize】
B.在Subscriber1项目中执行下面映射指令:
【Scaffold-DbContext "Server=localhost;Database=SubDB;User ID=root;Password=123456;" Pomelo.EntityFrameworkCore.MySql -OutputDir Models -UseDatabaseNames -DataAnnotations -NoPluralize】
(4).修改ConfigureService代码
A. EFCore自身注入:
services.AddDbContext<PubDBContext>(option => option.UseMySql(Configuration.GetConnectionString("EFStrMySQL"), Microsoft.EntityFrameworkCore.ServerVersion.Parse("5.7.28-mysql")));
B. Cap框架依赖MySQL:(下面选择一种即可)
x.UseEntityFramework<PubDBContext>(); //基于EFCore
x.UseMySql(Configuration.GetConnectionString("EFStrMySQL")); //基于ADO.Net (不需要依赖EF上下文,没有数据库的话事先建好即可)
发布者代码:(订阅者代码类似,上下文改为 SubDbContext)
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); //数据库连接(MySQL) services.AddDbContext<PubDBContext>(option => option.UseMySql(Configuration.GetConnectionString("EFStrMySQL"), Microsoft.EntityFrameworkCore.ServerVersion.Parse("5.7.28-mysql"))); //注册cap事件 services.AddCap(x => { //-----------------------------一.声明存储类型--------------------------------- //1. 使用MySQL存储 x.UseEntityFramework<PubDBContext>(); //x.UseMySql(Configuration.GetConnectionString("EFStrMySQL")); //-----------------------------二.声明消息队列类型---------------------------------//1.使用RabbitMq队列存储 x.UseRabbitMQ(rb => { rb.HostName = "localhost"; rb.UserName = "guest"; rb.Password = "guest"; rb.Port = 5672; rb.VirtualHost = "/"; //rb.QueueMessageExpires = 24 * 3600 * 10; //队列中消息自动删除时间(默认10天) }); //-----------------------------三.添加后台监控,用于人工干预---------------------------------//-----------------------------四.通用配置--------------------------------- }); }
发布者连接字符串:(订阅者代码类似,数据库改为:SubDB)
"ConnectionStrings": { "EFStrMySQL": "Server=localhost;Database=PubDB;User ID=root;Password=123456;" }
(5).测试:PostMan发送Get请求 http://localhost:9001/api/Pub/SendMsg ,测试通过,同时在数据库中自动生成两张表,其它运行效果与SQLServer完全相同啊。
A.cap.Published (Publisher1中只用该表)
B.cap.Received (Subscriber1中只用该表)
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。