在遇到与第三方系统做对接时,MQ无疑是非常好的解决方案(解耦、异步)。但是如果引入MQ组件,随之要考虑的问题就变多了,如何保证MQ消息能够正常被业务消费。所以引入MQ消费失败情况下,自动重试功能是非常重要的。这里不过细讲MQ有哪些原因会导致失败。

MQ重试,网上有方案一般采用的是,本地消息表+定时任务,不清楚的可以自行了解下。

我这里提供一种另外的思路,供大家参考。方案实现在RabbitMQ(安装延迟队列插件)+.NET CORE 3.1

设计思路为:

内置一个专门做重试的队列,这个队列是一个延迟队列,当业务队列消费失败时,将原始消息投递至重试队列,并设置延迟时间,当延迟时间到达后。重试队列消费会自动将消息重新投递会业务队列,如此便可以实现消息的重试,而且可以根据重试次数来自定义重试时间,比如像微信支付回调一样(第一次延迟3S,第二次延迟10S,第三次延迟60S),上面方案当然要保证MQ消费采用ACK机制。

那么如何让重试队列知道原来的业务队列是哪个,我们定义业务队列时,可以通过MQ的消息头内置一些信息:队列类型(业务队列也有可能是延迟队列)、重试次数(默认为 0)、交换机名称、路由键。业务队列消费失败时,将消息投递至重试队列时,则可以把业务队列的消息头传递至重试队列,那么重试队列消费,重新将消息发送给业务队列时,则可以知道业务队列所需要的所有参数(需要将重试次数+1)。

下面结合代码讲下具体实现:

我们先看看业务队列发送消息时,如何定义

IBasicProperties properties = channel.CreateBasicProperties();
properties.Persistent = true;
//初始化,需要内置一些消费异常,自动重试参数
if (headers == null)
{
headers = new Dictionary<string, object>();
}
//ttlSecond 有值表示消息将投递到延迟队列
//因为可以自建延迟队列,ttlSecond是业务标识
if (ttlSecond.HasValue)
{
if (!headers.ContainsKey("x-delay"))
{
headers.Add("x-delay", ttlSecond * 1000);
}
else
{
headers["x-delay"] = ttlSecond * 1000;
}
//queueType = 1表示延迟队列
//框架内部重试机制需要此参数,因为重新投递到原始队列时,需要区分普通队列还是延迟队列
if (!headers.ContainsKey("queueType"))
{
headers.Add("queueType", 1);
}
}
else
{
//queueType = 0表示普通队列
if (!headers.ContainsKey("queueType"))
{
headers.Add("queueType", 0);
}
}
//重试次数
if (!headers.ContainsKey("retryCount"))
{
headers.Add("retryCount", 0);
}
//原始交换机名称
if (!headers.ContainsKey("retryExchangeName"))
{
headers.Add("retryExchangeName", exchangeName);
}
//原始路由键
if (!headers.ContainsKey("retryRoutingKey"))
{
headers.Add("retryRoutingKey", routingKey);
}
properties.Headers = headers;
channel.BasicPublish(exchangeName, routingKey, properties, Encoding.UTF8.GetBytes(message));

这里会内置上面描述的重试队列需要的参数

再来看看业务队列消费如何处理,这里因为会自动重试,所以保证业务队列每次都是消费成功的(MQ才会将消息从队列中删除)

       //每次消费一条
channel.BasicQos(0, 1, false); //定义消费者
EventingBasicConsumer eventingBasicConsumer = new EventingBasicConsumer(channel);
eventingBasicConsumer.Received += async (sender, basicConsumer) =>
{
string body = Encoding.UTF8.GetString(basicConsumer.Body.ToArray());
Deadletter deadletter = null;
try
{
string errorMsg = await action(body);
if (!errorMsg.IsNullOrWhiteSpace())
{
deadletter = new Deadletter() { Body = body, ErrorMsg = errorMsg };
_logger.LogError($"业务队列消费异常(已知),消息头:{JsonUtils.Serialize(basicConsumer.BasicProperties.Headers)}{Environment.NewLine}原始消息:{body}{Environment.NewLine}错误:{errorMsg}");
}
}
catch (Exception ex)
{
deadletter = new Deadletter() { Body = body, ErrorMsg = ex.Message };
_logger.LogError(ex, $"业务队列消费异常(未知),消息头:{JsonUtils.Serialize(basicConsumer.BasicProperties.Headers)}{Environment.NewLine}原始消息:{body}");
}
//必定应答,不管消费成功还是失败
channel.BasicAck(basicConsumer.DeliveryTag, false);
//消费失败,投递消息至重试队列
if (deadletter != null)
{
PublishRetry(deadletter, basicConsumer.BasicProperties.Headers);
}
};

我们再看看PublishRetry重试队列的推送方法如何实现

IBasicProperties properties = channel.CreateBasicProperties();
properties.Persistent = true;
//x-delay为延迟队列的延迟时间
//如果第一次进行重试,请求头中是不存在延迟时间的,需要新增
//因为可以进行多次重试,所以第二次时,就会存在延迟时间
//但因为可以自建用于业务的延迟队列,所以自建的延迟队列,第一次重试也会存在x-delay,但是如果自建的延迟队列失败进行重试时,不能还使用自身的延迟时间,所以需要重新设置为系统默认的失败重试时间
if (!headers.ContainsKey("x-delay"))
{
headers.Add("x-delay", 0);
}
//重试次数
int retryCount = Convert.ToInt32(headers["retryCount"]);
//可以根据重试次数,实现上面说描述的微信回调的重试时间变长效果
headers["x-delay"] = retryCount * 1000;
properties.Headers = headers;
channel.BasicPublish(RETRY_EXCHANGE_NAME, string.Empty, properties, Encoding.UTF8.GetBytes(JsonUtils.Serialize(deadletter)));

重试队列的消费者实现

channel.BasicQos(0, 1, false);
EventingBasicConsumer eventingBasicConsumer = new EventingBasicConsumer(channel);
eventingBasicConsumer.Received += async (sender, basicConsumer) =>
{
string message = Encoding.UTF8.GetString(basicConsumer.Body.ToArray());
Deadletter deadletter = JsonUtils.Deserialize<Deadletter>(message);
IDictionary<string, object> headers = basicConsumer.BasicProperties.Headers;
//请求头中肯定会有如下参数,因为在框架代码中已经内置
//重试次数
int retryCount = Convert.ToInt32(headers["retryCount"]);
//原队列类型,如果原队列本身为延迟队列,重试投递的时候,必须也要为延迟队列,只是不需要延迟时间,投递回原队列后,会立马重新消费
int queueType = Convert.ToInt32(headers["queueType"]);
//原队列名称
string retryExchangeName = Encoding.UTF8.GetString((byte[])headers["retryExchangeName"]);
//原路由键
string retryRoutingKey = Encoding.UTF8.GetString((byte[])headers["retryRoutingKey"]);
if (retryCount <= 10)
{
headers["retryCount"] = retryCount + 1;
//原有队列为普通队列,重新投递时,也需要投递为普通队列类型
if (queueType == 0)
{
PublishMessage(retryExchangeName, retryRoutingKey, deadletter.Body, basicConsumer.BasicProperties.Headers);
}
//原有队列为延迟队列,重新投递时,也需要投递为延迟队列类型
else
{
PublishMessage(retryExchangeName, retryRoutingKey, deadletter.Body, basicConsumer.BasicProperties.Headers, 0);
}
}
//超过重试最大次数不再处理,交由外部委托来处理死信
else
{
await deadLetterTask(retryExchangeName, deadletter.Body, deadletter.ErrorMsg);
}
//应答
channel.BasicAck(basicConsumer.DeliveryTag, false);
};
//开启监听
channel.BasicConsume(RETRY_QUEUE_NAME, false, eventingBasicConsumer);

然后在系统中,内置重试队列消费者

//注册框架内自动重试
_rabbitMQClient.SubscribeRetry(async (exchangeName, message, errorMsg) =>
{
string content = $"原始交换机名称:{exchangeName}{Environment.NewLine}" +
$"原始消息内容:{message}{Environment.NewLine}" +
$"错误消息:{errorMsg}"; await PushWeChatMessage(content);
});

上述为我们MQ实现自动重试的一种方案,当然中间包括每次如果消费失败都可以发送通知,来通知业务人员关注消费失败的情况。可以自定义最大重试次数、重试间隔时间、死信的处理,这里仅仅是MQ重试机制的一种思路而已,大家如果有更好的方案,欢迎多多沟通。

MQ消费失败,自动重试思路的更多相关文章

  1. 精讲RestTemplate第8篇-请求失败自动重试机制

    本文是精讲RestTemplate第8篇,前篇的blog访问地址如下: 精讲RestTemplate第1篇-在Spring或非Spring环境下如何使用 精讲RestTemplate第2篇-多种底层H ...

  2. 精讲响应式WebClient第6篇-请求失败自动重试机制,强烈建议你看一看

    本文是精讲响应式WebClient第6篇,前篇的blog访问地址如下: 精讲响应式webclient第1篇-响应式非阻塞IO与基础用法 精讲响应式WebClient第2篇-GET请求阻塞与非阻塞调用方 ...

  3. Cypress系列(65)- 测试运行失败自动重试

    如果想从头学起Cypress,可以看下面的系列文章哦 https://www.cnblogs.com/poloyy/category/1768839.html 重试的介绍 学习前的三问 什么是重试测试 ...

  4. testng失败自动重试

    使用的监听类有:IRetryAnalyzer.TestListenerAdapter.IAnnotationTransformer public class Retry implements IRet ...

  5. rabbitmq~消息失败后重试达到 TTL放到死信队列(事务型消息补偿机制)

    这是一个基于消息的分布式事务的一部分,主要通过消息来实现,生产者把消息发到队列后,由消费方去执行剩下的逻辑,而当消费方处理失败后,我们需要进行重试,即为了最现数据的最终一致性,在rabbitmq里,它 ...

  6. .Net Core 商城微服务项目系列(十一):MQ消费端独立为Window服务+消息处理服务

    之前使用MQ的时候是通过封装成dll发布Nuget包来使用,消息的发布和消费都耦合在使用的站点和服务里,这样会造成两个问题: 1.增加服务和站点的压力,因为每次消息的消费就意味着接口的调用,这部分的压 ...

  7. Spring Cloud Stream消费失败后的处理策略(一):自动重试

    之前写了几篇关于Spring Cloud Stream使用中的常见问题,比如: 如何处理消息重复消费 如何消费自己生产的消息 下面几天就集中来详细聊聊,当消息消费失败之后该如何处理的几种方式.不过不论 ...

  8. Spring Cloud Stream消费失败后的处理策略(三):使用DLQ队列(RabbitMQ)

    应用场景 前两天我们已经介绍了两种Spring Cloud Stream对消息失败的处理策略: 自动重试:对于一些因环境原因(如:网络抖动等不稳定因素)引发的问题可以起到比较好的作用,提高消息处理的成 ...

  9. 「 从0到1学习微服务SpringCloud 」11 补充篇 RabbitMq实现延迟消费和延迟重试

    Mq的使用中,延迟队列是很多业务都需要用到的,最近我也是刚在项目中用到,就在跟大家讲讲吧. 何为延迟队列? 延迟队列就是进入该队列的消息会被延迟消费的队列.而一般的队列,消息一旦入队了之后就会被消费者 ...

随机推荐

  1. day08 索引的创建与慢查询优化

    day08 索引的创建与慢查询优化 昨日内容回顾 视图 视图:将SQL语句查询结果实体化保存起来,方便下次查询使用. 视图里面的数据来源于原表,视图只有表结构 # 创建视图 create view 视 ...

  2. 前端页面存放token

    //本地缓存,记录token function set(type, value) { localStorage.setItem(type, value); } function get(type) { ...

  3. 转 序列化Serializable和Parcelable的区别详解

    什么是序列化,为什么要进行序列化 答:对象要进行传输(如:activity 与activity间 ,网络间 进程间等等).存储到本地就必须进行序列化 . 这种可传输的状态就是序列化. 怎么序列化??两 ...

  4. mybatis-plus条件构造用is开头的Boolean类型字段时遇到的问题

    is打头的Boolean字段导致的代码生成器与lambda构造器的冲突 https://gitee.com/baomidou/mybatis-plus/issues/I171DD?_from=gite ...

  5. k8s之ansible安装

    项目地址:https://github.com/easzlab/kubeasz #:先配置harbor #:利用脚本安装docker root@k8s-harbor1:~# vim docker_in ...

  6. Linux基础命令---mget获取ftp文件

    mget 使用lftp登录mftp服务器之后,可以使用mget指令从服务器获取文件.mget指令可以使用通配符,而get指令则不可以.   1.语法       mget [-E]  [-a]  [- ...

  7. 京东消息中间件JMQ(转)

    http://blog.csdn.net/javahongxi/article/details/54411464 [京东技术]京东的MQ经历了JQ->AMQ->JMQ的发展,其中JQ的基于 ...

  8. vue 项目如何使用animate.css

    Animate.css是一款酷炫丰富的跨浏览器动画库,它在GitHub上的star数至今已有5.3万+. 在vue项目中我们可以借助于animate.css,用十分简单的代码来实现一个个炫酷的效果!( ...

  9. react18 来了,我 get 到...

    大家好! 本文主要是关于即将发布的 react 18 的新特性.那么 react18 带来了什么呢? 详情可以关注 github React 18 工作组仓库 1. automatic batchin ...

  10. Jenkins视图管理

    目录 一.简介 二.视图维护 创建视图 将项目加入视图中 三.状态图标变绿 四.看板 一.简介 在现在的编程中,公司往往采用的是模块化的编程方式,也就是说将一个项目拆分成许多模块,每个小项目组往往只负 ...