MQ消费失败,自动重试思路
在遇到与第三方系统做对接时,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消费失败,自动重试思路的更多相关文章
- 精讲RestTemplate第8篇-请求失败自动重试机制
本文是精讲RestTemplate第8篇,前篇的blog访问地址如下: 精讲RestTemplate第1篇-在Spring或非Spring环境下如何使用 精讲RestTemplate第2篇-多种底层H ...
- 精讲响应式WebClient第6篇-请求失败自动重试机制,强烈建议你看一看
本文是精讲响应式WebClient第6篇,前篇的blog访问地址如下: 精讲响应式webclient第1篇-响应式非阻塞IO与基础用法 精讲响应式WebClient第2篇-GET请求阻塞与非阻塞调用方 ...
- Cypress系列(65)- 测试运行失败自动重试
如果想从头学起Cypress,可以看下面的系列文章哦 https://www.cnblogs.com/poloyy/category/1768839.html 重试的介绍 学习前的三问 什么是重试测试 ...
- testng失败自动重试
使用的监听类有:IRetryAnalyzer.TestListenerAdapter.IAnnotationTransformer public class Retry implements IRet ...
- rabbitmq~消息失败后重试达到 TTL放到死信队列(事务型消息补偿机制)
这是一个基于消息的分布式事务的一部分,主要通过消息来实现,生产者把消息发到队列后,由消费方去执行剩下的逻辑,而当消费方处理失败后,我们需要进行重试,即为了最现数据的最终一致性,在rabbitmq里,它 ...
- .Net Core 商城微服务项目系列(十一):MQ消费端独立为Window服务+消息处理服务
之前使用MQ的时候是通过封装成dll发布Nuget包来使用,消息的发布和消费都耦合在使用的站点和服务里,这样会造成两个问题: 1.增加服务和站点的压力,因为每次消息的消费就意味着接口的调用,这部分的压 ...
- Spring Cloud Stream消费失败后的处理策略(一):自动重试
之前写了几篇关于Spring Cloud Stream使用中的常见问题,比如: 如何处理消息重复消费 如何消费自己生产的消息 下面几天就集中来详细聊聊,当消息消费失败之后该如何处理的几种方式.不过不论 ...
- Spring Cloud Stream消费失败后的处理策略(三):使用DLQ队列(RabbitMQ)
应用场景 前两天我们已经介绍了两种Spring Cloud Stream对消息失败的处理策略: 自动重试:对于一些因环境原因(如:网络抖动等不稳定因素)引发的问题可以起到比较好的作用,提高消息处理的成 ...
- 「 从0到1学习微服务SpringCloud 」11 补充篇 RabbitMq实现延迟消费和延迟重试
Mq的使用中,延迟队列是很多业务都需要用到的,最近我也是刚在项目中用到,就在跟大家讲讲吧. 何为延迟队列? 延迟队列就是进入该队列的消息会被延迟消费的队列.而一般的队列,消息一旦入队了之后就会被消费者 ...
随机推荐
- 从Redis分布式缓存实战入手到底层原理分析、面面俱到覆盖大厂面试考点
概述 官方说明 Redis官网 https://redis.io/ 最新版本6.2.6 Redis中文官网 http://www.redis.cn/ 不过中文官网的同步更新维护相对要滞后不少时间,但对 ...
- Spark(十一)【SparkSQL的基本使用】
目录 一. SparkSQL简介 二. 数据模型 三. SparkSQL核心编程 1. IDEA开发SparkSQL 2. SparkSession 创建 关闭 获取SparkContext 3. D ...
- 商业爬虫学习笔记day7-------解析方法之bs4
一.Beautiful Soup 1.简介 Beautiful Soup 是python的一个库,最主要的功能是从网页抓取数据.其特点如下(这三个特点正是bs强大的原因,来自官方手册) a. Beau ...
- hive 启动不成功,报错:hive 启动报 Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/hadoop/mapred/MRVersi
1. 现象:在任意位置输入 hive,准备启动 hive 时,报错: Exception in thread "main" java.lang.NoClassDefFoundErr ...
- 通过静态分析和持续集成 保证代码的质量 (Helix QAC)2
续上.... 第二章 部署示例:Jenkins and Helix QAC工具 第一节 Jenkins 作为持续集成系统 现在有很多持续集成工具,既有免费的,也有商业的.最近的研究显示,Jenkins ...
- Windows下mysql5.6升级到5.7的方法(亲测有效哦!)
Mysql的升级方式分为两种:原地升级和逻辑升级.这两种升级方式,本质没有什么区别的. 只是在对数据文件的处理上有些区别而已.原地升级是直接将数据文件进行拷贝,而逻辑升级对数据文件的处理方式是通过逻辑 ...
- 什么是JMS规范?
一.简介 JMS是什么:JMS是Java提供的一套技术规范和关于消息中间件的协议 JMS干什么用:通过生产者Producer,消息服务器,以及消费者通力合作,使异构系统能进行集成通信,缓解系统瓶颈,提 ...
- eslint使用和配置
1.全局安装 $ npm install -g eslint 2.初始化一个配置文件,得到一份文件名为 .eslintrc.js 的配置文件 eslint --init 3.配置.eslintrc.j ...
- LuoguP1619 解一元二次方程的烦恼 题解
Content 模拟一个系统,给出一个数 \(n\),让你判断是否是素数,如果是合数的话就要质因数分解. 需要注意的几点: 数字超过 \(4\times 10^7\),输出溢出提示. 数字小于 \(2 ...
- java 网络编程基础 UDP协议DatagramSocket,MulticastSocket 实现局域网聊天软件(Server + Client模式)
开发思路: 服务端两个Socket: UDP的DatagramSocket接收客户端消息. 广播multicastSocket 发送广播消息 客户端一个Socket: multicastSocke ...