RabbitMQ一个简单可靠的方案(.Net Core实现)

前言

  最近需要使用到消息队列相关技术,于是重新接触RabbitMQ。其中遇到了不少可靠性方面的问题,归纳了一下,大概有以下几种:

  1. 临时异常,如数据库网络闪断、http请求临时失效等;

  2. 时序异常,如A任务依赖于B任务,但可能由于调度或消费者分配的原因,导致A任务先于B任务执行;

  3. 业务异常,由于系统测试不充分,上线后发现某几个或某几种消息无法正常处理;

  4. 系统异常,业务中间件无法正常操作,如网络中断、数据库宕机等;

  5. 非法异常,一些伪造、攻击类型的消息。 

  针对这些异常,我采用了一种基于消息审计、消息重试、消息检索、消息重发的方案。

 

方案

 640?wx_fmt=png

 

  1. 消息均使用Exchange进行通讯,方式可以是direct或topic,不建议fanout。

  2. 根据业务在Exchange下分配一个或多个Queue,同时设置一个审计线程(Audit)监听所有Queue,用于记录消息到MongoDB,同时又不阻塞正常业务处理

  3. 生产者(Publisher)在发布消息时,基于AMQP协议,生成消息标识MessageId和时间戳Timestamp,根据消息业务添加头信息Headers便于跟踪。

  640?wx_fmt=png

  4. 消费者(Comsumer)消息处理失败时,则把消息发送到重试交换机(Retry Exchange),并设置过期(重试)时间及更新重试次数;如果超过重试次数则删除消息。

  5. 重试交换机Exchange设置死信交换机(Dead Letter Exchange),消息过期后自动转发到业务交换机(Exchange)。

  6. WebApi可以根据消息标识MessageId、时间戳Timestamp以及头信息Headers在MongoDB中对消息进行检索或重试。

   

  注:选择MongoDB作为存储介质的主要原因是其对头信息(headers)的动态查询支持较好,同等的替代产品还可以是Elastic Search这些。

 

生产者(Publisher)

  1. 设置断线自动恢复

  var factory = new ConnectionFactory{Uri = new Uri("amqp://guest:guest@192.168.132.137:5672"),AutomaticRecoveryEnabled = true  };

 

  2. 定义Exchange,模式为direct

  channel.ExchangeDeclare("Exchange", "direct");

 

  3. 根据业务定义QueueA和QueueB

  channel.QueueDeclare("QueueA", true, false, false);channel.QueueBind("QueueA", "Exchange", "RouteA");channel.QueueDeclare("QueueB", true, false, false);channel.QueueBind("QueueB", "Exchange", "RouteB");

 

  4. 启动消息发送确认机制,即需要收到RabbitMQ服务端的确认消息

  channel.ConfirmSelect();

 

  5. 设置消息持久化

  var properties = channel.CreateBasicProperties();properties.Persistent = true;

 

  6. 生成消息标识MessageId、时间戳Timestamp以及头信息Headers

  properties.MessageId = Guid.NewGuid().ToString("N");properties.Timestamp = new AmqpTimestamp(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());properties.Headers = new Dictionary<string, object>  {{ "key", "value" + i}};

 

  7. 发送消息,偶数序列发送到QueueA(RouteA),奇数序列发送到QueueB(RouteB)

  channel.BasicPublish("Exchange", i % 2 == 0 ? "RouteA" : "RouteB", properties, body);

 

  8. 确定收到RabbitMQ服务端的确认消息

  var isOk = channel.WaitForConfirms();if (!isOk){throw new Exception("The message is not reached to the server!");} 

  完整代码

640?wx_fmt=png

效果:QueueA和QueueB各一条消息,QueueAudit两条消息

 640?wx_fmt=png

   注:Exchange下必须先声明Queue才能接收到消息,上述代码并没有QueueAudit的声明;需要手动声明,或者先执行下面的消费者程序进行声明。

 

正常消费者(ComsumerA)

  1. 设置预取消息,避免公平轮训问题,可以根据需要设置预取消息数,这里是1

  _channel.BasicQos(0, 1, false);

  640?wx_fmt=png

 

  2. 声明Exchange和Queue

  _channel.ExchangeDeclare("Exchange", "direct");_channel.QueueDeclare("QueueA", true, false, false);_channel.QueueBind("QueueA", "Exchange", "RouteA");

 

  3. 编写回调函数

640?wx_fmt=png

 注:设置了RabbitMQ的断线恢复机制,当RabbitMQ连接不可用时,与MQ通讯的操作会抛出AlreadyClosedException的异常,导致主线程退出,哪怕连接恢复了,程序也无法恢复,因此,需要捕获处理该异常。

 

异常消费者(ComsumerB)

  1. 设置预取消息

  _channel.BasicQos(0, 1, false);

 

  2. 声明Exchange和Queue

  _channel.ExchangeDeclare("Exchange", "direct");_channel.QueueDeclare("QueueB", true, false, false);_channel.QueueBind("QueueB", "Exchange", "RouteB");

 

  3.  设置死信交换机(Dead Letter Exchange)

  var retryDic = new Dictionary<string, object>  {

      {"x-dead-letter-exchange", "Exchange"},{"x-dead-letter-routing-key", "RouteB"}};_channel.ExchangeDeclare("Exchange_Retry", "direct");_channel.QueueDeclare("QueueB_Retry", true, false, false, retryDic);_channel.QueueBind("QueueB_Retry", "Exchange_Retry", "RouteB_Retry");


  4. 重试设置,3次重试;第一次1秒,第二次10秒,第三次30秒

  _retryTime = new List<int>  {1 * 1000,10 * 1000,30 * 1000  };

 

  5. 获取当前重试次数

  var retryCount = 0;if (ea.BasicProperties.Headers != null && ea.BasicProperties.Headers.ContainsKey("retryCount")){retryCount = (int)ea.BasicProperties.Headers["retryCount"];_logger.LogWarning($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}]Message:{ea.BasicProperties.MessageId}, {++retryCount} retry started...");}

 

  6. 发生异常,判断是否可以重试

  private bool CanRetry(int retryCount){return retryCount <= _retryTime.Count - 1;} 

  7. 可以重试,则启动重试机制

640?wx_fmt=png

640?wx_fmt=png

审计消费者(Audit Comsumer)

  1. 声明Exchange和Queue

  _channel.ExchangeDeclare("Exchange", "direct");_channel.QueueDeclare("QueueAudit", true, false, false);_channel.QueueBind("QueueAudit", "Exchange", "RouteA");_channel.QueueBind("QueueAudit", "Exchange", "RouteB");

 

  2. 排除死信Exchange转发过来的重复消息

  if (ea.BasicProperties.Headers == null || !ea.BasicProperties.Headers.ContainsKey("x-death")){...}

 

  3. 生成消息实体

  var message = new Message{MessageId = ea.BasicProperties.MessageId,Body = ea.Body,Exchange = ea.Exchange,Route = ea.RoutingKey};

  4. RabbitMQ会用bytes来存储字符串,因此,要把头中bytes转回字符串

  if (ea.BasicProperties.Headers != null){var headers = new Dictionary<string, object>();foreach (var header in ea.BasicProperties.Headers){if (header.Value is byte[] bytes){headers[header.Key] = Encoding.UTF8.GetString(bytes);}else{headers[header.Key] = header.Value;}}message.Headers = headers;}

  5. 把Unix格式的Timestamp转成UTC时间

  if (ea.BasicProperties.Timestamp.UnixTime > 0){message.TimestampUnix = ea.BasicProperties.Timestamp.UnixTime;var offset = DateTimeOffset.FromUnixTimeMilliseconds(ea.BasicProperties.Timestamp.UnixTime);message.Timestamp = offset.UtcDateTime;}

 

  6. 消息存入MongoDB

  _mongoDbContext.Collection<Message>().InsertOne(message, cancellationToken: cancellationToken);

 

  MongoDB记录:

  640?wx_fmt=png

 

  重试记录:

  640?wx_fmt=png

 

消息检索及重发(WebApi)

  1. 通过消息Id检索消息

  640?wx_fmt=png

 

  2. 通过头消息检索消息

  640?wx_fmt=png

  640?wx_fmt=png

 

  3. 消息重发,会重新生成MessageId

  640?wx_fmt=png

  640?wx_fmt=png

 

Ack,Nack,Reject的关系

  1. 消息处理成功,执行Ack,RabbitMQ会把消息从队列中删除。

  2. 消息处理失败,执行Nack或者Reject:

  a) 当requeue=true时,消息会重新回到队列,然后当前消费者会马上再取回这条消息;

  b) 当requeue=false时,如果Exchange有设置Dead Letter Exchange,则消息会去到Dead Letter Exchange;

  c) 当requeue=false时,如果Exchange没设置Dead Letter Exchange,则消息从队列中删除,效果与Ack相同。

 

  3. Nack与Reject的区别在于:Nack可以批量操作,Reject只能单条操作。

  

RabbitMQ自动恢复

连接(Connection)恢复

  1. 重连(Reconnect)

  2. 恢复连接监听(Listeners)

  3. 重新打开通道(Channels)

  4. 恢复通道监听(Listeners)

  5. 恢复basic.qos,publisher confirms以及transaction设置

   

拓扑(Topology)恢复

  1. 重新声明交换机(Exchanges)

  2. 重新声明队列(Queues)

  3. 恢复所有绑定(Bindings)

  4. 恢复所有消费者(Consumers)

 

异常处理机制

  1. 临时异常,如数据库网络闪断、http请求临时失效等

  通过短时间重试(如1秒后)的方式处理,也可以考虑Nack/Reject来实现重试(时效性更高)。

 

  2. 时序异常,如A任务依赖于B任务,但可能由于调度或消费者分配的原因,导致A任务先于B任务执行

  通过长时间重试(如1分钟、30分钟、1小时、1天等),等待B任务先执行完的方式处理。

  

  3. 业务异常,由于系统测试不充分,上线后发现某几个或某几种消息无法正常处理

  等系统修正后,通过消息重发的方式处理。

 

  4. 系统异常,业务中间件无法正常操作,如网络中断、数据库宕机等

  等系统恢复后,通过消息重发的方式处理。

 

  5. 非法异常,一些伪造、攻击类型的消息

  多次重试失败后,消息从队列中被删除,也可以针对此业务做进一步处理。

 

源码地址

https://github.com/ErikXu/RabbitMesage

相关文章:

  • CAP带你轻松玩转ASP.NETCore消息队列

  • .NetCore Cap 结合 RabbitMQ 实现消息订阅

  • [译]RabbitMQ教程C#版 - 发布订阅

原文地址: https://www.cnblogs.com/Erik_Xu/p/9515208.html


.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com

640?wx_fmt=jpeg

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/320189.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

牛牛染颜色

链接&#xff1a; 文章目录题目描述题意&#xff1a;题解&#xff1a;核心代码&#xff1a;时间限制&#xff1a;C/C 1秒&#xff0c;其他语言2秒 空间限制&#xff1a;C/C 131072K&#xff0c;其他语言262144K 64bit IO Format: %lld题目描述 牛牛最近得到了一颗树&#xff0…

.netcore consul实现服务注册与发现-单节点部署

一、Consul的基础介绍Consul是HashiCorp公司推出的开源工具&#xff0c;用于实现分布式系统的服务发现与配置。与其他分布式服务注册与发现的方案&#xff0c;比如 Airbnb的SmartStack等相比&#xff0c;Consul的方案更“一站式”&#xff0c;内置了服务注册与发现框 架、分布一…

分布式事务解决方案以及 .Net Core 下的实现(上)

数据一致性是构建业务系统需要考虑的重要问题 &#xff0c; 以往我们是依靠数据库来保证数据的一致性。但是在微服务架构以及分布式环境下实现数据一致性是一个很有挑战的的问题。最近在研究分布式事物&#xff0c;分布式的解决方案有很多解决方案&#xff0c;也让我在研究的同…

微软MVP张善友告诉你,微服务选型要注意这些地方

周六的下午&#xff0c;广州周大福金融中心的写字楼静悄悄的&#xff0c;53楼的实盈多功能会议室却异常火爆&#xff0c;热闹非凡。来自广州各大科技公司的技术小伙伴们齐聚一堂&#xff0c;他们都在期待着一个人&#xff0c;那就是——微软MVP张善友和他带来的 .NET Core 微服…

像鱼

链接&#xff1a; 时间限制&#xff1a;C/C 1秒&#xff0c;其他语言2秒 空间限制&#xff1a;C/C 262144K&#xff0c;其他语言524288K 64bit IO Format: %lld题目描述 给你一个边长为 n 的用硬币摆成的实心三角形&#xff0c;请问把他倒过来最少需要多少步&#xff1f; 例子…

[ZJOI2005]午餐(贪心+dp)

首先若只有一个窗口&#xff0c;利用贪心&#xff0c;按吃饭时间从大到小排序即可 正确性证明&#xff1a; 定义 eat[i] 第i个人的吃饭时间&#xff0c;time[i] 第i个人的打饭时间 延长时间T[i]max(eat[i]- ∑ji1ntimej\sum\limits_{ji1}^ntime_jji1∑n​timej​ ,0) 最后…

【图论】【模板】静态仙人掌(luogu 5236)

【模板】静态仙人掌 题目大意 给你一个无向仙人掌图&#xff08;保证每条边至多出现在一个简单回路中的无向图&#xff09;&#xff0c;问你两个点之间的最短路距离 输入样例#1 9 10 2 1 2 1 1 4 1 3 4 1 2 3 1 3 7 1 7 8 2 7 9 2 1 5 3 1 6 4 5 6 1 1 9 5 7输出样例#1 5 …

SCF: 简单配置门面

Simple Configuration Facade, 简写为 SCF。是 代码 和 外部配置 (properties文件, 环境变量&#xff0c;系统/命令行参数, yaml文件, 等等)之间的一层抽象. 命名上和另一个著名组件slf4j (Simple Logging Facade for Java)相似, 在配置领域的地位也和slf4j &#xff08;.NET可…

【矩阵乘法】【倍增】美食家(luogu 6772)

美食家 题目大意 给你一个有向图&#xff0c;边权为经过所需时间 每个点有一个点权&#xff0c;有些点还有有特殊的点权 当你到达一个点后&#xff0c;可以获得该点的点权&#xff08;重复经过可以重复获得&#xff0c;但不能停留&#xff09;&#xff0c;若在某个时间到某个…

.net core实践系列之短信服务-为什么选择.net core(开篇)

前言从今天我将会写.net core实战系列&#xff0c;以我最近完成的短信服务作为例子。该系列将会尽量以最短的时间全部发布出来。源码也将优先开源出来给大家。源码地址&#xff1a;https://github.com/SkyChenSky/Sikiro.SMS.NET CORE简介ASP.NET Core 是一个跨平台的高性能开源…

.netcore consul实现服务注册与发现-集群完整版

一、Consul的集群介绍Consul Agent有两种运行模式&#xff1a;Server和Client。这里的Server和Client只是Consul集群层面的区分&#xff0c;与搭建在Cluster之上的应用服务无关&#xff0c; 以Server模式运行的Consul Agent节点用于维护Consul集群的状态&#xff0c;官方建议每…

浅谈surging服务引擎中的rabbitmq组件和容器化部署

1、前言上个星期完成了surging 的0.9.0.1 更新工作&#xff0c;此版本通过nuget下载引擎组件&#xff0c;下载后&#xff0c;无需通过代码build集成&#xff0c;引擎会通过Sidecar模式自动扫描装配异构组件来构建服务引擎&#xff0c;而这篇将介绍浅谈surging服务引擎中的rabbi…

csp-j/s总结

文章目录csp-jcsp-s总结csp-j T1傻逼题(我是傻逼&#xff09;&#xff0c;手残把&打成整除了&#xff08;大样例还对了gg&#xff09; T2乱推&#xff0c;然后打了个O(n)O(n)O(n)&#xff0c;却WA了15分&#xff1f; T3看了看&#xff0c;修改不会相互影响&#xff1f;那不…

.net core实践系列之短信服务-架构设计

前言上篇《.net core实践系列之短信服务-为什么选择.net core&#xff08;开篇&#xff09;》简单的介绍了&#xff08;水了一篇&#xff09;.net core。这次针对短信服务的架构设计和技术栈的简析。源码地址&#xff1a;https://github.com/SkyChenSky/Sikiro.SMS为什么需要架…

CAP 2.3版本发布,支持 MongoDB

前言经过2个月的调整及测试&#xff0c;CAP 2.3 版本终于发布了&#xff0c;这个版本最大的特性就是对于 MongoDB 的支持&#xff0c;感谢博客园团队的keke同学对于 MongoDB 支持所提供的 PR&#xff0c;相信随着博客园的使用&#xff0c;CAP 会越来越多的帮助到更多的人。CAP …

.NETCore 实现容器化Docker与私有镜像仓库管理

一、Docker介绍Docker是用Go语言编写基于Linux操作系统的一些特性开发的&#xff0c;其提供了操作系统级别的抽象&#xff0c;是一种容器管理技术&#xff0c;它隔离了应用程序对基础架构&#xff08;操作系统等&#xff09;的依赖。相较于虚拟机而言&#xff0c;Docker共享的是…

记一次.net core 集成vue 实践

简介以前我们总是喜欢用服务端渲染前端&#xff08;虽然也有htmlajax这种做法&#xff09;&#xff0c;后来node诞生了&#xff0c;前端工具链&#xff08;不如说生态&#xff09;诞生了&#xff0c;大家都开始了前端的重构之路&#xff0c;本文将以一简单案例介绍下我们是如何…

AspNetCore中使用Ocelot之 IdentityServer4

前言&#xff1a;Ocelot网关是基于AspNetCore开发的可扩展的高性能的企业级Api网关&#xff0c;目前已经基于2.0 升级版本升级&#xff0c;在使用AspNetCore 开发的时候可以使用2.0版本了&#xff0c;开源项目Ocelot 张大队长是主力的参与人员&#xff0c;以前提起张大队前面都…

Nginx主配置文件详解【笔记】

1 Nginx配置文件目录结构 最近使用Nginx做负载均衡&#xff0c;顺便整理Nginx配置文件笔记如下所述&#xff0c;本文主要梳理主配置文件各属性的作用。使用yum安装好Nginx后&#xff0c;可以在默认路径&#xff08;/etc/nginx&#xff09;看到如下配置文件。 /etc/nginx/ ├─…

微服务介绍及Asp.net Core实战项目系列之微服务介绍

0、目录整体架构目录&#xff1a;ASP.NET Core分布式项目实战-目录一、微服务选型在做微服务架构的技术选型的时候&#xff0c;以“无侵入”和“社区活跃”为主要的考量点&#xff0c;将来升级为原子服务架构、量子服务架构的时候、甚至恢复成单体架构的时候&#xff0c;代价最…