前言

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

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

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

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

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

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

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

方案

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

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

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

  

  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 %  ==  ? "RouteA" : "RouteB", properties, body);

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

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

  完整代码

var factory = new ConnectionFactory
{
Uri = new Uri("amqp://guest:guest@localhost:5672"),
AutomaticRecoveryEnabled = true
}; using (var connection = factory.CreateConnection())
{
using (var channel = connection.CreateModel())
{
channel.ExchangeDeclare("Exchange", "direct"); channel.QueueDeclare("QueueA", true, false, false);
channel.QueueBind("QueueA", "Exchange", "RouteA"); channel.QueueDeclare("QueueB", true, false, false);
channel.QueueBind("QueueB", "Exchange", "RouteB"); channel.ConfirmSelect(); for (var i = ; i < ; i++)
{
var properties = channel.CreateBasicProperties();
properties.Persistent = true;
properties.MessageId = Guid.NewGuid().ToString("N");
properties.Timestamp = new AmqpTimestamp(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); properties.Headers = new Dictionary<string, object>
{
{ "key", "value" + i}
}; var message = "Hello " + i;
var body = Encoding.UTF8.GetBytes(message); channel.BasicPublish("Exchange", i % == ? "RouteA" : "RouteB", properties, body);
var isOk = channel.WaitForConfirms();
if (!isOk)
{
throw new Exception("The message is not reached to the server!");
}
}
}
}

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

 

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

正常消费者(ComsumerA)

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

  _channel.BasicQos(, , false);

  

  2. 声明Exchange和Queue

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

  3. 编写回调函数

  var consumer = new EventingBasicConsumer(_channel);
  consumer.Received += (model, ea) =>
  {
  //The QueueA is always successful.
  try
  {
  _channel.BasicAck(ea.DeliveryTag, false);
  }
  catch (AlreadyClosedException ex)
  {
  _logger.LogCritical(ex, "RabbitMQ is closed!");
  }
  };   _channel.BasicConsume("QueueA", false, consumer);

  注:设置了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>
  {
   * ,
   * ,
   *
  };

  5. 获取当前重试次数

  var retryCount = ;
  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 - ;
  }

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

  private void SetupRetry(int retryCount, string retryExchange, string retryRoute, BasicDeliverEventArgs ea)
  {
  var body = ea.Body;
  var properties = ea.BasicProperties;
  properties.Headers = properties.Headers ?? new Dictionary<string, object>();
  properties.Headers["retryCount"] = retryCount;
  properties.Expiration = _retryTime[retryCount].ToString();   try
  {
  _channel.BasicPublish(retryExchange, retryRoute, properties, body);
  }
  catch (AlreadyClosedException ex)
  {
  _logger.LogCritical(ex, "RabbitMQ is closed!");
  }
  }

  完整代码

    _channel.BasicQos(, , false);

    _channel.ExchangeDeclare("Exchange", "direct");
_channel.QueueDeclare("QueueB", true, false, false);
_channel.QueueBind("QueueB", "Exchange", "RouteB"); 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"); var consumer = new EventingBasicConsumer(_channel);
consumer.Received += (model, ea) =>
{
//The QueueB is always failed.
bool canAck;
var retryCount = ;
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...");
} try
{
Handle();
canAck = true;
}
catch (Exception ex)
{
_logger.LogCritical(ex, "Error!");
if (CanRetry(retryCount))
{
SetupRetry(retryCount, "Exchange_Retry", "RouteB_Retry", ea);
canAck = true;
}
else
{
canAck = false;
}
} try
{
if (canAck)
{
_channel.BasicAck(ea.DeliveryTag, false);
}
else
{
_channel.BasicNack(ea.DeliveryTag, false, false);
}
}
catch (AlreadyClosedException ex)
{
_logger.LogCritical(ex, "RabbitMQ is closed!");
}
}; _channel.BasicConsume("QueueB", false, consumer);

审计消费者(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 > )
  {
  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记录:

  

  重试记录:

  

消息检索及重发(WebApi)

  1. 通过消息Id检索消息

  

  2. 通过头消息检索消息

  

  

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

  

  

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

RabbitMQ一个简单可靠的方案(.Net Core实现)的更多相关文章

  1. Kubernetes使用prometheus+grafana做一个简单的监控方案

    前言 本文介绍在k8s集群中使用node-exporter.prometheus.grafana对集群进行监控.其实现原理有点类似ELK.EFK组合.node-exporter组件负责收集节点上的me ...

  2. 一个简单可靠的CRC 计算程序

    //注:此函数主要的特点为准,快,体积小,非常适合于8位单片机 int16 crc_1021(int16 old_crc, int8 data) {   int16 crc;   int16 x;   ...

  3. ASP.NET Core Web API下事件驱动型架构的实现(一):一个简单的实现

    很长一段时间以来,我都在思考如何在ASP.NET Core的框架下,实现一套完整的事件驱动型架构.这个问题看上去有点大,其实主要目标是为了实现一个基于ASP.NET Core的微服务,它能够非常简单地 ...

  4. .net core api +swagger(一个简单的入门demo 使用codefirst+mysql)

    前言: 自从.net core问世之后,就一直想了解.但是由于比较懒惰只是断断续续了解一点.近段时间工作不是太忙碌,所以偷闲写下自己学习过程.慢慢了解.net core 等这些基础方面学会之后再用.n ...

  5. C#中缓存的使用 ajax请求基于restFul的WebApi(post、get、delete、put) 让 .NET 更方便的导入导出 Excel .net core api +swagger(一个简单的入门demo 使用codefirst+mysql) C# 位运算详解 c# 交错数组 c# 数组协变 C# 添加Excel表单控件(Form Controls) C#串口通信程序

    C#中缓存的使用   缓存的概念及优缺点在这里就不多做介绍,主要介绍一下使用的方法. 1.在ASP.NET中页面缓存的使用方法简单,只需要在aspx页的顶部加上一句声明即可:  <%@ Outp ...

  6. .Net Core下使用RabbitMQ比较完备的两种方案(虽然代码有点惨淡,不过我会完善)

    一.前言     上篇说给大家来写C#和Java的方案,最近工作也比较忙,迟到了一些,我先给大家补上C#的方案,另外如果没看我上篇博客的人最好看一下,否则你可能看的云里雾里的,这里我就不进行具体的方案 ...

  7. asp.net core 实现一个简单的仓储

    一直有自己写个框架的想法,但是一直没有行动起来,最近比较闲,正好可以开工了. 现在已经完成了两部分.1.一个简单仓储,实现使用的是ef 2.IOC部分,这里是把内置的ioc替换成了aotofac,这部 ...

  8. 网络游戏开发-服务器(01)Asp.Net Core中的websocket,并封装一个简单的中间件

    先拉开MSDN的文档,大致读一遍 (https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/websockets) WebSocket 是一 ...

  9. 并发编程概述 委托(delegate) 事件(event) .net core 2.0 event bus 一个简单的基于内存事件总线实现 .net core 基于NPOI 的excel导出类,支持自定义导出哪些字段 基于Ace Admin 的菜单栏实现 第五节:SignalR大杂烩(与MVC融合、全局的几个配置、跨域的应用、C/S程序充当Client和Server)

    并发编程概述   前言 说实话,在我软件开发的头两年几乎不考虑并发编程,请求与响应把业务逻辑尽快完成一个星期的任务能两天完成绝不拖三天(剩下时间各种浪),根本不会考虑性能问题(能接受范围内).但随着工 ...

随机推荐

  1. python图形用户

    1)使用GUI 1.GUI:Graphical user interface 2.tkinter:GUI libary for Python自带的库 3.GUI:Example 2)Ubuntu18. ...

  2. atomic详解

    http://www.360doc.com/content/14/1120/21/203028_426770242.shtml

  3. jsonpCallback: xx is not a function

    参考文献: https://www.cnblogs.com/lenghan/p/5777588.html 根据原理解读,发现同一个页面如果多个ajax请求调用的jsonpCallback名字相同的话, ...

  4. DirectX11 With Windows SDK--25 法线贴图

    前言 在很早之前的纹理映射中,纹理存放的元素是像素的颜色,通过纹理坐标映射到目标像素以获取其颜色.但是我们的法向量依然只是定义在顶点上,对于三角形面内一点的法向量,也只是通过比较简单的插值法计算出相应 ...

  5. 移动开发常用head部分

    <!--[viewport] 设置布局viewport的各种信息: width=device-width:布局viewport宽度等于设备宽度 initial-scale=1.0:默认缩放比为1 ...

  6. Flask-Login用户登陆

    Flask-Login Flask-Login 提供用户会话管理,处理常见的登录.退出和注册的任务. Flask-Login 没有绑定数据库,可以从数据库回调用户对象. 安装flask-login p ...

  7. stringify()和parse()的区别

    记录一下小知识点,大神略过... 一:JSON.parse()[从一个字符串中解析出json对象] 例子: //定义一个字符串 var data='{"name":"go ...

  8. CNN的反向传播

    在一般的全联接神经网络中,我们通过反向传播算法计算参数的导数.BP 算法本质上可以认为是链式法则在矩阵求导上的运用.但 CNN 中的卷积操作则不再是全联接的形式,因此 CNN 的 BP 算法需要在原始 ...

  9. Saltstack自动化操作记录(2)-配置使用 【转】

    之前梳理了Saltstack自动化操作记录(1)-环境部署,下面说说saltstack配置及模块使用: 为了试验效果,再追加一台被控制端minion机器192.168.1.118需要在master控制 ...

  10. spring mybatis mysql 事务不起作用

    之前框架事务应该是好的,不过这次做了些修改,不知如何竟然不好用了,整理了好半天,java框架配置就是吓人,有一点不熟悉的就可能暴露问题,好处是又深入的了解了配置原理. 开始以为是mysql不支持事务的 ...