记录几个RabbitMQ使用过程中容易踩的那些坑:
简要代码如下,设置消息自动ack,这种情况下,MQ只要确认消息发送成功,无须等待应答就会丢弃消息,
这会导致客户端还未处理完时,出异常或断电了,导致消息丢失的后果,
解决方法就是把代码里的true,改成false,并在消息处理完后发ack响应。
// 要监听队列,所以不能用using关闭channel通道
var channel = GetChannel();
var consumer = new EventingBasicConsumer(channel);
channel.BasicConsume(queue, true, consumer); // 消息自动ack
注:自动ack还有个弊端,只要队列不空,RabbitMQ会源源不断的把消息推送给客户端,而不管客户端能否消费的完
为了解决问题1,做了改进,简要代码如下:
var channel = GetChannel();
var consumer = new EventingBasicConsumer(channel);
// 开启acknowledge机制,在接收事件里ack
channel.BasicConsume(queue, false, consumer);
consumer.Received += (sender, e) =>
{
try
{
callback(e.Body, e.BasicProperties.Headers);
// 无异常时,发ack通知mq丢弃消息
((EventingBasicConsumer)sender).Model.BasicAck(e.DeliveryTag, false);
}
catch (Exception exp)
{
LogHelper.Info(exp);
}
};
这段代码中,先处理消息,完成后,再做ack响应,失败就不做ack响应,这样消息会储存在MQ的Unacked消息里,不会丢失,看起来没啥问题,
但是有一次,callback触发了一个bug,导致所有消息都抛出异常,然后队列的Unacked消息数暴涨,导致MQ响应越来越慢,甚至崩溃的问题。
原因是如果MQ没得到ack响应,这些消息会堆积在Unacked消息里,不会抛弃,直至客户端断开重连时,才变回ready;
如果Consumer客户端不断开连接,这些Unacked消息,永远不会变回ready状态,Unacked消息多了,占用内存越来越大,就会异常了。
解决办法就是及时去ack消息了
为了解决问题2,再调整一下代码,简要代码如下:
var channel = GetChannel();
var consumer = new EventingBasicConsumer(channel);
// 开启acknowledge机制,在接收事件里ack
channel.BasicConsume(queue, false, consumer);
consumer.Received += (sender, e) =>
{
try
{
callback(e.Body, e.BasicProperties.Headers);
((EventingBasicConsumer) sender).Model.BasicAck(e.DeliveryTag, false);
}
catch (Exception exp)
{
LogHelper.Info(exp);
// 出错了,发nack,并通知MQ把消息塞回的队列头部(不是尾部)
((EventingBasicConsumer) sender).Model.BasicNack(e.DeliveryTag, false, true);
}
};
嗯,改成这模样总没问题了吧,正常就ack,不正常就nack,并等下一次重新消费。
果然,又出问题了,这回又是callback出异常了,但是故障现象是Ready的消息猛增,一直不见减少。
原因是出异常后,把消息塞回队列头部,下一步又消费这条会出异常的消息,又出错,塞回队列……
进入了死循环了,当然新的消息不会消费,导致堆积了……
这个咋办?只能不用nack,所有消息都ack,自己记录日志,后续走其它job恢复日志了。
就是把catch里的BasicNack改成BasicAck。
这个问题跟前面的3个没啥联系,简要代码如下:
var channel = GetChannel();
// 启用QoS,每次预取5条消息,避免消息处理不过来,全部堆积在本地缓存里
channel.BasicQos(0, 5, false);
var consumer = new EventingBasicConsumer(channel);
// 开启acknowledge机制,在接收事件里ack,配合qos进行流控
channel.BasicConsume(queue, false, consumer);
consumer.Received += (sender, e) =>
{
try
{
callback(e.Body, e.BasicProperties.Headers);
((EventingBasicConsumer) sender).Model.BasicAck(e.DeliveryTag, false);
}
catch (Exception exp)
{
LogHelper.Info(exp);
}
};
这段代码中,由于开启了QoS,当RabbitMQ的队列达到5条Unacked消息时,不会再推送消息给Consumer,
如果回调函数出异常了,就会导致消息无法ack,从而导致无法继续处理后续的消息。
你问解决办法?当然是参考问题3,全部消息都去做ack响应呗(异常里也用Ack,而不用Nack)。
在RabbitMQ的.Net 3.6.9版本驱动里,不支持异步处理消息的方法,如果预取了10条消息,这10条消息会在本地缓存里,一条一条串行处理,效率比较低下,
在5.0以后的驱动里添加了AsyncEventingBasicConsumer类的支持,
但是我们还在用3.6.9,只能自己去用多线程搞了,简要代码如下:
var channel = GetChannel();
// 启用QoS,每次预取5条消息,避免消息处理不过来,全部堆积在本地缓存里
channel.BasicQos(0, 5, false);
var consumer = new EventingBasicConsumer(channel);
// 开启acknowledge机制,在接收事件里ack,配合qos进行流控
channel.BasicConsume(queue, false, consumer);
consumer.Received += (sender, e) =>
{
ThreadPool.UnsafeQueueUserWorkItem(state => {
try
{
callback(e.Body, e.BasicProperties.Headers);
}
catch (Exception exp)
{
LogHelper.Info(exp);
}
finally
{
((EventingBasicConsumer) sender).Model.BasicAck(e.DeliveryTag, false);
}
}, null);
};
RequestedHeartbeat要设置为5~20秒,我的项目中默认是设置为10秒
具体问题和解决,请参考:https://blog.csdn.net/youbl/article/details/79024061