MASA Framework - EventBus设计
目录
MASA Framework - 整体设计思路
MASA Framework - EventBus设计
概述
利用发布订阅模式来解耦不同架构层级,亦可用于解决隔离业务之间的交互
优点:
- 松耦合
- 横切关注点
- 可测试性
- 事件驱动
发布订阅模式
发布者通过调度中心将消息发送给订阅者。调度中心解决发布与订阅者之间的关系,保证消息可以送达订阅者手中。
发布者与订阅者互不相识,发布者只管向调度中心发布消息,而订阅者只关心自己订阅的消息类型
多订阅者保序执行
在常见的发布订阅模式中,的确很少见到类似的说法。但在实际业务中我们会有类似的需求,一个消息由调度中心协调多个订阅者按照顺序执行消息,同时还可以将上一个订阅者处理过的消息传递给下一个订阅者。这样既可以保留发布订阅模式的特性,又有了顺序执行逻辑的特性。
一个小思考:如果EventBus的配置支持动态调整的话,是否业务的执行顺序也可以被动态排列组合?
换句话说它或许可以为进程内工作流提供了一个可能性
Event Sourcing(事件溯源)
一种事件驱动的架构模式,可以用于审计和溯源
- 基于事件驱动架构
- 以事件为事实
- 业务数据由事件计算产生的视图,可以持久化也可以不持久化
CQRS(命令查询的责任分离)
CQRS是一种架构模式,能够使改变模型与查询模型的实现分离
Event Sourcing & CQRS
事件溯源可以与CQRS很好的配合
- 在Command Handler中持久化事件到Event Store的同时实时计算一个最终视图给View DB用于查询展示
- 在Query中既可以通过View DB获取最新状态,也可以通过Event Store来重放事件来校验View或用于更严谨的业务
Saga
Saga是一个长活事务被分解成可以交错运行的子事务集合。其中每个子事务都是一个保持数据库一致性的真实事务
- 每个Saga由一系列sub-transaction Ti 组成
- 每个Ti 都有对应的补偿动作Ci,补偿动作用于撤销Ti造成的结果
两种执行顺序
- T1, T2, T3...[Tx retry]...,Tn
- T1, T2, ..., Tj, Cj,..., C2, C1
两种恢复策略
- backward recovery,向后恢复,补偿所有已完成的事务,如果任一子事务失败。即上面提到的第二种执行顺序,其中j是发生错误的sub-transaction,这种做法的效果是撤销掉之前所有成功的sub-transation,使得整个Saga的执行结果撤销
- forward recovery,向前恢复,重试失败的事务,假设每个子事务最终都会成功。适用于必须要成功的场景,执行顺序是类似于这样的:T1, T2, ..., Tj(失败), Tj(重试),..., Tn,其中j是发生错误的sub-transaction。该情况下不需要Ci
BuildingBlocks的类视图
作为接口标准,BuildingBlocks中并没有过多的干涉实现方式,它只保留了最基础的功能流程限制,以达到最小EventBus的功能集合。至于最终是基于接口还是特性来实现订阅关系的,交还给Contrib自行决定。
事件
用于本地事件的发布/订阅
IEvent
:事件接口,IEvent<TResult>
为带返回值的基础事件接口IEventHanldler<TEvent>
:事件处理器接口,ISagaEventHandler<TEvent>
为Saga的实现提供了基础接口要求IMiddleware<TEvent>
:中间件接口,允许在事件执行前挂载预处理动作和时间执行后的收尾动作IEventBus
:事件总线接口,用于发送事件,并提供订阅关系维护和附加功能执行
集成事件
用于跨进程事件的发布/订阅
IntegrationEventLog
:集成事件日志,用于实现本地消息表的消息模型IIntegrationEventLogService
:集成事件日志服务接口ITopic
:发布/订阅的主题IIntegrationEvent
:集成事件接口IIntegrationEventBus
:集成事件总线,用于跨进程调用的事件总线
CQRS
用于使改变模型与查询模型的实现分离
IQuery<TResult>
:查询的接口IQueryHandler<TCommand,TResult>
:查询处理器接口ICommand
:可用于增删改等指令的接口ICommandHandler<TCommand>
:指令处理器接口
Event Bus
要完成上述的这些功能,我们需要借助于EventBus,它需要有以下基础功能
- 接收事件
- 维护订阅关系
- 转发事件
接收与转发事件
这两个功能其实可以合并为一个接口,由发布者调用Publish,再由Event Bus根据订阅关系转发即可
维护订阅关系
在.Net项目中,我们常见的用于扫描自动注册的方式是接口
和特性
MediatR支持接口的方式去扫描事件订阅关系,举个例子:IRequestHandler<,>
public class PingHandler : IRequestHandler<Ping, string>
{
public Task<string> Handle(Ping request, CancellationToken cancellationToken)
{
return Task.FromResult("Pong");
}
}
如果你的代码洁癖程度没有高的离谱,或许你希望是这样
public class NetHandler : IRequestHandler<Ping, string>, IRequestHandler<Telnet, string>
{
public Task<string> Handle(Ping request, CancellationToken cancellationToken)
{
return Task.FromResult("Pong");
}
public Task<string> Handle(Telnet request, CancellationToken cancellationToken)
{
return Task.FromResult("Success");
}
}
看着好像还行?如果很多呢?
那有没有办法解决这个问题?
特性!我们来看个例子
public class NetHandler
{
[EventHandler]
public Task PingAsync(PingEvent @event)
{
//TODO
}
[EventHandler]
public Task TelnetAsync(TelnetEvent @event)
{
//TODO
}
}
似乎我们找到了一个出路
多订阅者保序执行
通过事件层层推进确实可以满足顺序执行的场景,但如果你被大量无限套娃的事件包围的时候或许你需要另外一个出路,看下例子:
public class NetHandler
{
[EventHandler(0)]
public Task PingAsync(PingEvent @event)
{
//TODO
}
[EventHandler(1)]
public Task LogAsync(PingEvent @event)
{
//TODO
}
}
只要参数是同一个Event就会按照EventHandler的Order顺序执行。
Saga
那执行失败了怎么办,如果两个方法因为其中一个需要调用远程服务而无法跟本地事务结合,能帮我回滚吗?
来吧,SAGA走起,帮你再做个取消动作,同时还支持重试机制,以及是否忽略当前步骤的取消动作。
我们先来预设一下场景:
- 调用CheckBalanceAsync来检查余额
- 调用WithdrawAsync, 抛出 exception
- 重试WithdrawAsync 3 次
- 调用CancelWithdrawAsync
代码如下:
public class TransferHandler
{
[EventHandler(1)]
public Task CheckBalanceAsync(TransferEvent @event)
{
//TODO
}
[EventHandler(2, FailureLevels.ThrowAndCancel, enableRetry: true, retryTimes: 3)]
public Task WithdrawAsync(TransferEvent @event)
{
//TODO
throw new Exception();
}
[EventHandler(2, FailureLevels.Ignore, enableRetry: false, isCancel: true)]
public Task CancelWithdrawAsync(TransferEvent @event)
{
//TODO
}
}
AOP
举个业务场景,给所有Command在执行前增加一个参数验证
我们提供了Middleware,允许像俄罗斯套娃一样(.Net Middleware)做横切关注点的相关的事情
public class LoggingMiddleware<TEvent>
: IMiddleware<TEvent> where TEvent : notnull, IEvent
{
private readonly ILogger<LoggingMiddleware<TEvent>> _logger;
public LoggingMiddleware(ILogger<LoggingMiddleware<TEvent>> logger) => _logger = logger;
public async Task HandleAsync(TEvent @event, EventHandlerDelegate next)
{
_logger.LogInformation("----- Handling command {EventName} ({@Event})", typeof(TEvent).FullName, @event);
await next();
}
}
注册DI
builder.Services.AddTransient(typeof(IMiddleware<>), typeof(LoggingMiddleware<>))
MASA EventBus完整功能列表
- 接收事件
- 维护订阅关系 - 接口
- 维护订阅关系 - 特性
- 多订阅者顺序执行
- 转发事件
- Saga
- AOP
- UoW
- 自动开启和关闭事务
Integration Event Bus
用于跨服务的Event Bus,支持最终一致性,本地消息表
Pub/Sub
提供了Pub Sub接口,并基于Dapr Pub/Sub提供默认实现
本地消息表
提供了本地消息保存和UoW联动接口,并基于EF Core提供默认实现
使用方法
启用Dapr Event Bus
builder.Services
.AddDaprEventBus<IntegrationEventLogService>(options=>
{
options.UseUoW<CatalogDbContext>(dbOptions => dbOptions.UseSqlServer("server=localhost;uid=sa;pwd=Password;database=test"))
.UseEventLog<CatalogDbContext>();
)
});
定义Integration Event
public class DemoIntegrationEvent : IntegrationEvent
{
public override string Topic { get; set; } = nameof(DemoIntegrationEvent);//dapr topic name
//todo other properties
}
定义DbContext(非必须,定义DbContext可以将本地消息表与业务事务联动)
public class CustomDbContext : IntegrationEventLogContext
{
public DbSet<User> Users { get; set; } = null!;
public CustomDbContext(MasaDbContextOptions<CustomDbContext> options) : base(options)
{
}
}
发送 Event
IIntegrationEventBus eventBus; // from DI
await eventBus.PublishAsync(new DemoIntegrationEvent());
订阅 Event(基于Dapr Pub/Sub的版本)
[Topic("pubsub", nameof(DomeIntegrationEvent))]
public async Task DomeIntegrationEventHandleAsync(DomeIntegrationEvent @event)
{
//todo
}
Domain Event Bus
在领域中同时提供Event Bus和Integration Event Bus的能力,允许实时发送事件或在Save时一次性触发
Domain Event Bus是最完整的能力,所以使用Domain Event Bus相当于已经开启了Event Bus和Integration Event Bus,在Domain Event Bus内部会自动协调事件分类往Event Bus和Integration Event Bus分流
启用Domain Event Bus
builder.Services
.AddDomainEventBus(options =>
{
options.UseEventBus()//Use in-process events
.UseUoW<CustomDbContext>(dbOptions => dbOptions.UseSqlServer("server=localhost;uid=sa;pwd=P@ssw0rd;database=idientity"))
.UseDaprEventBus<IntegrationEventLogService>()///Use cross-process events
.UseEventLog<LocalMessageDbContext>()
.UseRepository<CustomDbContext>();
})
添加DomainCommand
Domain Event是进程内事件,IntegrationDomainEvent是跨进程事件
public class RegisterUserSucceededIntegrationEvent : IntegrationDomainEvent
{
public override string Topic { get; set; } = nameof(RegisterUserSucceededIntegrationEvent);
public string Account { get; set; } = default!;
}
public class RegisterUserSucceededEvent : DomainEvent
{
public string Account { get; set; } = default!;
}
进程内事件订阅
[EventHandler]
public Task RegisterUserHandlerAsync(RegisterUserDomainCommand command)
{
//TODO
}
跨进程事件订阅
[Topic("pubsub", nameof(RegisterUserSucceededIntegrationEvent))]
public async Task RegisterUserSucceededHandlerAsync(RegisterUserSucceededIntegrationEvent @event)
{
//todo
}
发送DomainCommand
IDomainEventBus eventBus;//from DI
await eventBus.PublishAsync(new RegisterUserDomainCommand());
使用场景
- 兼顾遗留系统对接
- 游走在云与非云中
- 流计算
- 微服务解耦和跨集群通信(需要将Dapr Pub/Sub改为Dapr Binding,不难)
- 部分AOP类场景
总结
事件驱动可以解决一些特定场景的问题,凡事都有两面性,在本来就很简单的业务场景中使用如此复杂的模式会带来不小的负担。
学以致用,学无止境。
开源地址
MASA.BuildingBlocks:https://github.com/masastack/MASA.BuildingBlocks
MASA.Contrib:https://github.com/masastack/MASA.Contrib
MASA.Utils:https://github.com/masastack/MASA.Utils
MASA.EShop:https://github.com/masalabs/MASA.EShop
如果你对我们的MASA Framework感兴趣,无论是代码贡献、使用、提Issue,欢迎联系我们
MASA Framework - EventBus设计的更多相关文章
- MASA Framework - DDD设计(1)
目录 MASA Framework - 整体设计思路 MASA Framework - EventBus设计 MASA Framework - MASA Framework - DDD设计(1) DD ...
- MASA Framework - DDD设计(2)
目录 MASA Framework - 整体设计思路 MASA Framework - EventBus设计 MASA Framework - MASA Framework - DDD设计(1) MA ...
- MASA Framework -- EventBus入门与设计
概述 事件总线是一种事件发布/订阅结构,通过发布订阅模式可以解耦不同架构层级,同样它也可以来解决业务之间的耦合,它有以下优点 松耦合 横切关注点 可测试性 事件驱动 发布订阅模式 通过下图我们可以快速 ...
- MASA Framework - 整体设计思路
源起 年初我们在找一款框架,希望它有如下几个特点: 学习成本低 只需要学.Net每年主推的技术栈和业务特性必须支持的中间件,给开发同学减负,只需要专注业务就好 个人见解:一款好用的框架应该是补充,而不 ...
- MASA Auth - 权限设计
权限术语 Subject:用户,用户组 Action:对Object的操作,如增删改查等 Object:权限作用的对象,也可以理解为资源 Effect:规则的作用,如允许,拒绝 Condition:生 ...
- 通过Azure bot framework composer 设计一个AI对话机器人bot(查询天气)
本文介绍通过机器人框架设计器 (Bot framework composer)接近拖拉拽的方式设计一个聊天机器人,该聊天机器人的主要功能是发起http请求查询天气.当然,稍微变通下,可以用来查询几乎任 ...
- 1. 连接字符串的创建 - Lazy.Framework从零开始设计自己的ORM架构
开发初衷 注册了博客园已经有几个月了,却从来都没有上来过,本人大概从2010年开始就开始做.NET 方向的开发. 这个是我在博客园发布的第一个帖子. 主要就是说说最近在写的一个ORM架构. 本人接触的 ...
- EntityFramework 学习 一 Entity Framework 查询设计
First/FirstOrDefault: using (var ctx = new SchoolDBEntities()) { var student = (from s in ctx.Studen ...
- DDD-领域驱动设计简谈
看到网上讨论 DDD 的文章越来越多,咱也不能甘于人后啊,以下是我对 DDD 的个人理解,短小精悍,不喜忽喷. 也谈DDD(领域驱动设计) 解决什么问题 传统模式,产品评审结束,开发人员就凭经验拆分模 ...
随机推荐
- 层次分析法、模糊综合评测法实例分析(涵盖各个过程讲解、原创实例示范、MATLAB源码公布)
目录 一.先定个小目标 二.层次分析法部分 2.1 思路总括 2.2 构造两两比较矩阵 2.3 权重计算方法 2.3.1 算术平均法求权重 2.3.2 几何平均法求权重 2.3.3 特征值法求权重 2 ...
- Pytorch入门下 —— 其他
本节内容参照小土堆的pytorch入门视频教程. 现有模型使用和修改 pytorch框架提供了很多现有模型,其中torchvision.models包中有很多关于视觉(图像)领域的模型,如下图: 下面 ...
- 采集 base64 编码的图片
问题 爬虫抓取网页的时候,遇到有的图片是 base64 编码的格式,要怎样下载到本地呢? 示例:base64 编码的 img 标签 <!-- 内容太长省略一部分 --> <img s ...
- LuoguP7072 [CSP-J2020] 直播获奖 题解
Update \(\texttt{2020.11.13}\) 修改了一个小细节. \(\texttt{2020.11.16}\) 修改了一个错误. Content 有一场 \(n\) 个人的比赛,计划 ...
- 常用故障排查监控shell脚本
#!/bin/bash #ping_monitor.sh IP_ADDRESS=$1 if [ -n "$IP_ADDRESS" ] ; then while : do PING_ ...
- 【LeetCode】122.Best Time to Buy and Sell Stock II 解题报告(Java & Python & C++)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 日期 题目地址:https://leetcode.c ...
- 【LeetCode】474. Ones and Zeroes 解题报告(Python)
[LeetCode]474. Ones and Zeroes 解题报告(Python) 作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ ...
- 【LeetCode】621. Task Scheduler 解题报告(Python & C++)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 公式法 日期 题目地址:https://leetco ...
- MySQL 中常见的时间类型有三种 DATE, DATETIME 和 TIMESTAMP
MySQL 中常见的时间类型有三种 DATE, DATETIME 和 TIMESTAMP,其中 DATE 类型用于表示日期,但是不会包含时间,格式为 YYYY-MM-DD,而 DATETIME 和 T ...
- 第十七个知识点:描述和比较DES和AES的轮结构
第十七个知识点:描述和比较DES和AES的轮结构 这是密码学52件事中的第17篇.本周我们描述和比较DES和AES的结构. DES和AES都是迭代分组密码的例子.分组密码通过重复使用一个简单的轮函数来 ...