在日常工作中使用RabbitMQ偶尔会遇不可预料的情况导致的消息积压,一般出现消息积压基本上分为几种情况:

  1. 消费者消费消息的速度赶不上生产速度,这总问题主要是业务逻辑没设计好消费者和生产者之间的平衡,需要改业务流程或逻辑已保证消费度跟上生产消息的速,譬如增加消费者的数量等。

  2. 消费者出现异常,导致一直无法接收新的消息,这种问题需要排查消费的逻辑是不是又问题,需要优化程序。

除了上面的者两种问题,还有一些其他情况会导致消息积压,譬如一些系统是无法预计成产消息的速度和频率,又或者消费者的速度已经被限制,不能通过加新的消费者来解决,譬如不同的系统间的API对接,对接那一方就做了请求频率的限制,或者对方系统承受不了太大的并发,还有一些系统如果是面对企业客户,譬如电商,物流,仓储等类似平台系统的客户的下单是没有规律的或者集中某一个时间段下单的,这种就不能简单的通过加消费者来解决,就需要分析具体业务来避免消息积压。

针对这种情况,我想到了4中解决思路:

  1. 拆分MQ,生产者一个MQ,消费者一个MQ,写一个程序监听生产者的MQ模拟消费速度(譬如线程休眠),然后发送到消费者的MQ,如果消息积压则只需要处理生产者的MQ的积压消息,不影响消费者MQ

  2. 拆分MQ,生产者一个MQ,消费者一个MQ,写一个程序监听生产者的MQ,定义一个全局静态变量记录上一次消费的时间,如果上一次时间和当前时间只差小于消费者的处理时间,则发送到一个延迟队列(可以使用死信队列实现)发送到消费者的MQ,如果消息积压则只需要处理生产者的MQ的积压消息,不影响消费者MQ

  3. 使用Redis的List或ZSET做接收消息缓存,写一个程序按照消费者处理时间定时从Redis取消息发送到MQ

  4. 设置消息过期时间,过期后转入死信队列,写一个程序处理死信消息(重新如队列或者即使处理或记录到数据库延后处理)

其中使用延时队列会相对来说逻辑简单,业务逻辑变更也不大,在RabbitMQ中,可使用死信来及延时队列插件rabbitmq_delayed_message_exchange两种方式实现延时队列。

使用插件可以在官网找到:https://www.rabbitmq.com/community-plugins.html

插件的安装及使用方式就不做介绍了,主要介绍下使用死信来实现延时队列,原理就是将消息发送到一个死信队列,并设置过期时间,过期后将死信转发到要处理的消息队列。

生产者相关代码:

          /// <summary>
/// 发送延时队列消息
/// </summary>
/// <param name="message"></param>
/// <param name="queueName"></param>
/// <param name="prefetchCount">默认20</param>
public void SendDelayQueues(string message, string queueName,double delayMilliseconds,string beDeadLetterPrefix="beDeadLetter_")
{
#region 死信到期后转入的交换机及队列
//死信转入新的队列的路由键(消费者使用的路由键)
var routingKey = queueName;
var exchangeName = queueName;
//定义队列
Channel.QueueDeclare(queue: queueName,
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);
//定义交换机
Channel.ExchangeDeclare(exchange: exchangeName,
type: "direct");
//队列绑定到交换机
Channel.QueueBind(queue: queueName,
exchange: exchangeName,
routingKey: routingKey);
#endregion //将变成死信的队列名
var beDeadLetterQueueName = beDeadLetterPrefix + queueName;
//将变成死信的交换机名
var beDeadLetterExchangeName = beDeadLetterPrefix + queueName; //定义一个有延迟的交换机来做死信(该消息不能有消费者,不然无法变成死信)
Channel.ExchangeDeclare(exchange:beDeadLetterExchangeName ,
type: "direct"); //定义该延迟消息过期变成死信后转入的交换机(消费者需要绑定的交换机)
//Channel.ExchangeDeclare(exchange: queueName,type: "direct"); var dic = new Dictionary<string, object>();
//dic.Add("x-expires", 30000);
//dic.Add("x-message-ttl", 12000);//队列上消息过期时间,应小于队列过期时间
dic.Add("x-dead-letter-exchange", queueName);//变成死信后转向的交换机
dic.Add("x-dead-letter-routing-key",routingKey);//变成死信后转向的路由键
//定义将变成死信的队列
Channel.QueueDeclare(queue: beDeadLetterQueueName,
durable: true,
exclusive: false,
autoDelete: false,
arguments: dic); //队列绑定到交换机
Channel.QueueBind(queue: beDeadLetterQueueName,
exchange: beDeadLetterExchangeName,
routingKey: routingKey); //不要同时给一个消费者推送多于prefetchCount个消息, ushort prefetchCount = 20
//Channel.BasicQos(prefetchSize: 0, prefetchCount: prefetchCount, global: false);
var body = Encoding.UTF8.GetBytes(message);
var properties = Channel.CreateBasicProperties();
properties.Persistent = true;
properties.DeliveryMode = 2;//持久化消息
//过期时间
properties.Expiration = delayMilliseconds.ToString();
Channel.BasicPublish(exchange: beDeadLetterExchangeName,
routingKey: routingKey,
basicProperties: properties,
body: body);
}

消费者相关代码:

        /// <summary>
/// 设置延迟队列接收的事件
/// </summary>
/// <param name="action"></param>
/// <param name="queueName"></param>
/// <param name="prefetchCount">默认1</param>
/// <param name="autoAck"></param>
/// <param name="consumerCount"></param>
public void SetDelayQueuesReceivedAction(Action<string> action, string queueName, ushort prefetchCount = 1,
bool autoAck = false, int consumerCount = 1)
{
if (prefetchCount < 1)
{
throw new Exception("consumerCount must be greater than 1 !");
} var exchangeName = queueName;
var routingKey = queueName;
for (int i = 0; i < consumerCount; i++)
{
var Channel = Connection.CreateModel();
//定义队列
Channel.QueueDeclare(queue: queueName,
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);
//定义交换机
Channel.ExchangeDeclare(exchange: exchangeName,
type: "direct");
//队列绑定到交换机
Channel.QueueBind(queue: queueName,
exchange: exchangeName,
routingKey: routingKey);
//不要同时给一个消费者推送多于prefetchCount个消息
Channel.BasicQos(prefetchSize: 0, prefetchCount: prefetchCount, global: false);
ChannelList.Add(Channel);
var consumer = new EventingBasicConsumer(Channel);
consumer.Received += (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
//Console.WriteLine("处理消费者ConsumerTag:" + ea.ConsumerTag);
action(message);
//手动确认消息应答
Channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);
};
//autoACK自动消息应答设置为false
Channel.BasicConsume(queue: queueName, autoAck: autoAck, consumer: consumer);
}
}

完整代码实现放到了Github:https://github.com/tanyongzheng/TZ.RabbitMQ

随机推荐

  1. java提高篇(二三)-----HashMap

    HashMap也是我们使用非常多的Collection,它是基于哈希表的 Map 接口的实现,以key-value的形式存在.在HashMap中,key-value总是会当做一个整体来处理,系统会根据 ...

  2. AFNetworking讲解

    #import "ViewController.h" //#import "AFNetworking/AFNetworking.h" #import " ...

  3. 传统的Ado.net 参数设置:params SqlParameter[] commandParameters

    C#代码  ExecuteReader(string connectionString, CommandType commandType, string commandText, params Sql ...

  4. 浅谈Qt事件的路由机制:鼠标事件

    请注意,本文是探讨文章而不是教程,是根据实验和分析得出的结果,可能是错的,因此欢迎别人来探讨和纠正. 这几天对于Qt的事件较为好奇,平时并不怎么常用,一般都是用信号,对于事件的处理,一般都是需要响应键 ...

  5. js计时函数实现秒表的开始-暂停-清零功能

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  6. spring cloud 配置zuul实用

    在线演示 演示地址:http://139.196.87.48:9002/kitty 用户名:admin 密码:admin 技术背景 前面我们通过Ribbon或Feign实现了微服务之间的调用和负载均衡 ...

  7. matlab读取cvs文件的几种方法

    matlab读取CVS文件的几种方法: 1,实用csvread()函数   csvread()函数有三种使用方法: 1.M = csvread('filename')2.M = csvread('fi ...

  8. 有关Java字符集编码的问题

    在Java语言中,下列关于字符集编码(Character set encoding)和国际化(i18n)的问题,哪些是正确的? A.每个中文字符占用2个字节,每个英文字符占用1个字节 B.假设数据库中 ...

  9. L323 英语有必要学语法吗

    The Agony and Ecstasy of Grammar “Underline a relative clause.” This challenge would give a lot of a ...

  10. 学JS的心路历程 - PixiJS -基础(一)

    建立canvas 今天开始我们一步步来看怎么使用PixiJS吧! 在开始之前,要先提醒各位需要先运行webserver,否则将会遇到一些奇怪的问题喔! 最基本的canvas画布是肯定需要的,Pixi提 ...