aspnetcore微服务种服务之间的通信一般都有用到消息中间件,如何确保该服务的持久层保存创建的数据同时又把消息成功投递到了关联服务,关联服务做对应的处理。

下面就以一个简单的例子来演示实现方式之一,即发件箱模式。

下面解决方案有两个服务,做演示用的比较简单,一个是订单服务,一个是账单服务。完成订单的同时把订单信息通过本例的rabbitmq发送到billapi服务中去。

首先trading服务有一个领域内事件接收器

 public abstract class IEntity
{
private int id;
public virtual int Id
{
get { return id; }
protected set { id = value; }
} private List<IEvent> _domainEvents;
public IReadOnlyCollection<IEvent> DomainEvents => _domainEvents?.AsReadOnly();
public void AddDomainEvent(IEvent eventItem)
{
_domainEvents = _domainEvents ?? new List<IEvent>();
_domainEvents.Add(eventItem);
}
public void RemoveDomainEvent(IEvent eventItem)
{
_domainEvents?.Remove(eventItem);
}
public void ClearDomainEvents()
{
_domainEvents?.Clear();
}
}
 public class CreateOrderEvent:IEvent
{
public Guid EventId { get; set; }
public int CustomerId { get; set; }
public CreateOrderEvent(Guid EventId,int customerId)
{
this.EventId = EventId;
CustomerId = customerId;
} }

我把事件简化到实体类里面,也可以不需要这个IEntity,那每次都需要自己创建order的同时创建一个事件,当然事件集合需要自己定义存起来。

发件箱顾名思义就是所有邮件定时定期的投递到邮箱中,定时定期的取出来往需要的地方去投递。

这里的邮件就是事件了,而投递就是事件发布。

这个实例的事件放到实体类种有领域的味道,因为在一个领域order内可以把关联的事件都放一起。下面代码就是借助efcore的拦截器来统一在savechange的地方来把事件写到数据库中去。

我新建一个order,同时把发布的order事件存到数据,这就是发件箱模式。好多数据库和中间件操作的最终一致性大体都是这个模式,借助数据库的分布式事务。

  public sealed class OutBoxMessageInterceptor:SaveChangesInterceptor
{
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)
{
DbContext? dbContxt = eventData.Context;
if (dbContxt is null)
{
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
var events = dbContxt.ChangeTracker.Entries<IEntity>().Select(x => x.Entity).SelectMany(x =>
{
List<IEvent> entities = new List<IEvent>();
foreach (var item in x.DomainEvents)
{
if(!(item is null))
entities.Add(item);
}
x.ClearDomainEvents();
return entities;
}).Select(x => new OutBoxMessage
{
Id = Guid.NewGuid(),
OccurredOnUtc = DateTime.UtcNow,
Type = x.GetType().Name,
Content = System.Text.Json.JsonSerializer.Serialize((CreateOrderEvent)x)
}).ToList();
if(events!=null && events.Any())
dbContxt.Set<OutBoxMessage>().AddRangeAsync(events);
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
}

数据库拦截器注入的代码少不了,写是写进去了,下面就是怎么去往另外的服务的发布呢?

 builder.Services.AddDbContext<TradingDbContext>((sp,ops) =>
{
ops.UseSqlServer("Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=Traing;Integrated Security=True;Connect Timeout=30;Encrypt=False;Trust Server Certificate=False;Application Intent=ReadWrite;Multi Subnet Failover=False");
var interceptor = sp.GetService<OutBoxMessageInterceptor>();
ops.AddInterceptors(interceptor);
}, ServiceLifetime.Scoped);

这里就是后台任务去取数据做处理了

 public class OutBoxMessageBackgroundService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly IRabbitMQEventBus _publisher;
public OutBoxMessageBackgroundService(IServiceProvider serviceProvider, IRabbitMQEventBus publisher)
{
_serviceProvider = serviceProvider;
_publisher = publisher;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var scope = _serviceProvider.CreateScope();
var _orderingContext = scope.ServiceProvider.GetService<TradingDbContext>();
var messages = await _orderingContext.Set<OutBoxMessage>().Where(m => m.ProceddedOnUtc == null)
.Take(10).ToListAsync(stoppingToken);
foreach (var message in messages)
{
if (string.IsNullOrEmpty(message.Content))
continue;
var retries = 3;
var retry = Policy.Handle<Exception>()
.WaitAndRetry(
retries,
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
(exception, timeSpan, retry, ctx) =>
{
Console.WriteLine($"发布时间失败:{message}");
});
retry.Execute(() => _publisher.Publish(new { Content=message.Content,Id = message.Id }, exchange: "RabbitMQ.EventBus.Simple", routingKey: "rabbitmq.eventbus.test"));
message.ProceddedOnUtc = DateTime.UtcNow;
}
await _orderingContext.SaveChangesAsync(stoppingToken);
}
}

就是这么简单,tradinfgapi的任务就这么愉快地完成了,这里保证了数据库写数据和发布事件出去最终是同步的,即使服务出问题重启也一样能完成任务。

下面就是接受事件的billapi的服务了,因为上面代码用来重试机制,而且其他情况也比面不了事件重复发送,下面就简单的处理下订阅事件的幂等性。

 public class IDomainEvent : IEvent
{
public Guid Id { get; set; }
public string Content { get; set; }
}
public class IdempotentDomainEventHandler : IEventResponseHandler<IDomainEvent,int>,IDisposable
{
private readonly IServiceProvider _serviceProvider;
public IdempotentDomainEventHandler(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
} public void Dispose()
{
Console.WriteLine("MessageBodyHandle Disposable.");
} public async Task<int> HandleAsync(HandlerEventArgs<IDomainEvent> args)
{
using var scope = _serviceProvider.CreateScope();
BillingDbContext _context = scope.ServiceProvider.GetService<BillingDbContext>();
string consumer = args.GetType().Name;
if (await _context.Set<OutboxMessageConsumer>().AnyAsync(o => o.Guid == args.EventObject.Id && o.Name==consumer))
{
return default;
}
Console.WriteLine($"等待处理的消息{args.EventObject.Content}");
CreateOrderEvent createOrderEvent = System.Text.Json.JsonSerializer.Deserialize<CreateOrderEvent>(args.EventObject.Content);
await _context.BillingRecords.AddAsync(new BillingRecord { CreateTime=DateTime.UtcNow, OrderEventId=createOrderEvent.EventId});
Console.WriteLine($"处理的消息完毕"); _context.Set<OutboxMessageConsumer>().Add(new OutboxMessageConsumer
{
Guid = args.EventObject.Id,
Name = consumer
});
return await _context.SaveChangesAsync();
}
} public class OutboxMessageConsumer
{
public int Id { get; set; }
public Guid Guid { get; set; }
public string Name { get; set; }
}
 /// <summary>
/// 来自tradingapi的数据
/// </summary>
public class CreateOrderEvent
{
public Guid EventId { get; set; }
public int CustomerId { get; set; }
}

同样是把事件处理后写入到数据库,每次进来去数据库看看有没有,就这么简单的完成了事件订阅的重复处理。

下面运行一下程序看看效果,创建order前billingrecord是没有记录的。

这里出现了一个喜闻乐见的事情,trading服务已经发布了事件,billing服务没收到,可能是rabbitmq卡住了,不过没关系,因为有这个发件箱模式可以重启下服务,这个时间丢不了。

重启了下服务就消费掉了这条数据。

至于重复消费的测试就省了,有需要自己下载源码去测试

liuzhixin405/outboxpattern: microservice (github.com)

aspnetcore微服务中使用发件箱模式实例的更多相关文章

  1. 通什翡翠商城大站协议邮件群发系统日发20-30万封不打码不换ip不需发件箱100%进收件箱

    用一种新的技术思维去群发邮件一种不用换IP,不需要任何发件箱的邮件群发方式一种不需要验证码,不需要**代码变量的邮件群发方式即使需要验证码也能全自动识别验证码的超级智能软件教你最核心的邮件群发思维和软 ...

  2. .NET CORE微服务中CONSUL的相关使用

    .NET CORE微服务中CONSUL的相关使用 1.consul在微服务中的作用 consul主要做三件事:1.提供服务到ip的注册 2.提供ip到服务地址的列表查询 3.对提供服务方做健康检查(定 ...

  3. 懒人邮件群发日发50-100万封不打码不换IP不需发件箱大站协议系统营销软件100%进收件箱

    用一种新的技术思维去群发邮件 一种不用换IP,不需要任何发件箱的邮件群发方式 一种不需要验证码,不需要**代码变量的邮件群发方式 即使需要验证码也能全自动识别验证码的超级智能软件 教你最核心的邮件群发 ...

  4. 微服务中的健康监测以及其在ASP.NET Core服务中实现运行状况检查

    1 .什么是健康检查? 健康检查几乎就是名称暗示的.它是一种检查您的应用程序是否健康的方法.随着越来越多的应用程序转向微服务式架构,健康检查变得尤其重要(Health Check).虽然微服务架构有很 ...

  5. 谈谈微服务中的 API 网关(API Gateway)

    前言 又是很久没写博客了,最近一段时间换了新工作,比较忙,所以没有抽出来太多的时间写给关注我的粉丝写一些干货了,就有人问我怎么最近没有更新博客了,在这里给大家抱歉. 那么,在本篇文章中,我们就一起来探 ...

  6. Spring Cloud微服务中网关服务是如何实现的?(Zuul篇)

    导读 我们知道在基于Spring Cloud的微服务体系中,各个微服务除了在内部提供服务外,有些服务接口还需要直接提供给客户端,如Andirod.IOS.H5等等. 而一个很尴尬的境地是,如果直接将提 ...

  7. AspNetCore微服务下的网关-Kong(一)

    Kong是Mashape开源的高性能高可用API网关和API服务管理层.它基于OpenResty,进行API管理,并提供了插件实现API的AOP.Kong在Mashape 管理了超过15,000 个A ...

  8. 微服务中的 API 网关(API Gateway)

    API 网关(API Gateway)提供高性能.高可用的 API 托管服务,帮助用户对外开放其部署在 ECS.容器服务等云产品上的应用,提供完整的 API 发布.管理.维护生命周期管理.用户只需进行 ...

  9. 在spring boot微服务中使用JWS发布webService

    发布时间:2018-11-22   技术:Java+spring+maven   概述 在springboot微服务中使用JWS发布webService,在服务启动时自动发布webservice接口. ...

  10. 微服务中的CAP定律

    说到微服务,先给大家提一下CAP分布式应用知识吧,无论你微服务使用的是阿里云开源的Dubbo还是基于Springboot的一整套实现微服务的Springcloud都必须遵循CAP定理不然你所实现的分布 ...

随机推荐

  1. 国内 IoT 物联网平台终局的思考:未来只会有 3家

    随着物联网成为国家十四五规划的新型基础设施,许多企业纷纷投资物联网赛道,希望能提前布局物联网平台,打造一个新的生态出来. 十年后,那些投入重金的物联网平台最终能存活下来几家呢? 01. 公有云IoT平 ...

  2. 20211306 2021-2022-2 《Python程序设计》第二次实验报告

    20211306 2021-2022-2 <Python程序设计>第二次实验报告 课程:<Python程序设计> 班级:2113 姓名:丁文博 学号:20211306 实验教师 ...

  3. c++学习2 基础关键词

    三 volatile强制访问内存 在一个变量的频繁使用中,系统为了提高效率,会自动将内存里面的数据放入CPU里的寄存器里.但在某些特殊场景下,放入寄存器这个操作反倒会导致CPU无法及时获取最新的一手数 ...

  4. 将本地文件复制到docker 容器中

    查询容器id: docker ps 查询完整容器id docker inspect -f '{{.ID}}'短容器id cp docker cp 本地路径 完整容器ID:容器路径例: docker c ...

  5. sqlsever中使用的 select top n在mysql 语句中如何更改

    string sqlSelect = "select top(3) ROW_NUMBER() over(order by UserTime) as Num,* from " + & ...

  6. 洛谷P1203 坏掉的项链

    洛谷P1203 坏掉的项链 首先看到这题的数据样例,3<=n<=350,不是水水暴搜就过了嘛 不难想到暴力解,先破环成链 因为可能有全部都可换成一种颜色的情况,考虑每次遇到s[i]==s[ ...

  7. Ubuntu系统update时提示源不安全被禁用的一种解决办法

    参考自这篇文章Ubuntu系统update时提示源不安全被禁用 - 知乎 (zhihu.com). 安装好Ubuntu18.04并更换清华源后,在运行 sudo apt update 更新源时报错如下 ...

  8. 看懂插入才算看懂redis跳表

    跳表的数据结构 forward相当于9之于8,12之于8 插入算法流程: 1.初始化两个数组update和rank数组:update表示每一层的前继节点.rank表示update节点到头结点的距离(用 ...

  9. api接口基础Day2

    精华笔记: 正则表达式: 用于描述字符串的内容格式,使用它通常用于匹配一个字符串是否符合格式要求 正则表达式的语法:-------------了解.不用纠结.不用深入研究 1.[]:表示一个字符,该字 ...

  10. Windows 解决teamview远程必须mstsc连接

    真实原因是你的TeamViewer一直在用远程桌面的ID进行登录,所以一旦远程桌面断开,TeamViewer就无法连接了.因此我们只需要切换为服务器的TeamViewer ID即可,服务器的TeamV ...