RabbitMQ 消息确认机制 - 消费者确认
由于生产者和消费者不直接通信,生产者只负责把消息发送到队列,消费者只负责从队列获取消息(不管是push还是pull).
消息被"消费"后,是需要从队列中删除的.那怎么确认消息被"成功消费"了呢?
是消费者从队列获取到消息后,MQ服务器 就从队列中删除该消息?
那如果消费者收到消息后,还没来得及"消费"它,或者说还没来得及进行业务逻辑处理时,消费者所在的channel信道或者连接因某种原因断开了,
那这条消息岂不是就被无情的抛弃了...
我们更期望的是,消费者从队列获取到消息后,MQ服务器 暂时不删除该条消息,
等到消费者"成功消费"掉该消息后,再删除它.
所以需要一个机制来确认生产者发送的消息被消费者"成功消费".
RabbitMQ 提供了一种叫做"消费者确认"的机制.
消费者确认
消费者确认分两种:自动确认和手动确认.
在自动确认模式中,消息在发送到消费者后即被认为"成功消费".这种模式可以降低吞吐量(只要消费者可以跟上),以降低交付和消费者处理的安全性.这种模式通常被称为“即发即忘”.与手动确认模型不同,如果消费者的TCP连接或通道在真正的"成功消费"之前关闭,则服务器发送的消息将丢失.因此,自动消息确认应被视为不安全,并不适用于所有工作负载.
使用自动确认模式时需要考虑的另一件事是消费者过载.手动确认模式通常与有界信道预取(BasicQos方法)一起使用,该预取限制了信道上未完成(“进行中”)的消息的数量.但是,自动确认没有这种限制.因此,消费者可能会被消息的发送速度所淹没,可能会导致消息积压并耗尽堆或使操作系统终止其进程.某些客户端库将应用TCP反压(停止从套接字读取,直到未处理的交付积压超过某个限制).因此,仅建议能够以稳定的速度有效处理消息的消费者使用自动确认模式.
注:生产端发送消息给RabbitMq服务器,与消费者从RabbitMq服务器取消息,都是相互独立的。
生产者给RabbitMq服务器发送消息,如果启用了消息确认模式,那也是RabbitMq服务器给生产者返回一个Ack,或者NaAck与消费端没任何关系
消费者从RabbitMq服务器消费消息,调用了这段channel.BasicAck(deliveryTag: basic.DeliveryTag, multiple: false);代码,也仅仅表示,消费者告诉RabbitMq服务器,我已经却认收到了消息。它与生产者没有任何关系。 很多初学者搞不清楚他们的关系,容易造成误解。
ProducterApp:生产者端的消息确认
在生产端创建一个发送消息的MqHelper类
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Collections.Generic;
using System.Text;
namespace RabbitMqApp
{
/// <summary>
/// RabbitMQ消息队列处理
/// </summary>
public class MqHelper
{
/// <summary>
/// RabbitMQ地址
/// </summary>
private string HostName = "192.168.31.30"; //ConfigurationManager.AppSettings["RabbitMQHostName"];
/// <summary>
/// 账号
/// </summary>
private string UserName = "admin"; //ConfigurationManager.AppSettings["RabbitMQUserName"];
/// <summary>
/// 密码
/// </summary>
private string Password = "admin"; // ConfigurationManager.AppSettings["RabbitMQPassword"];
/// <summary>
/// 端口号
/// </summary>
private int Prot = 5672;
/// 连接配置
private ConnectionFactory connfactory { get; set; } //创建一个工厂连接对象
public MqHelper()
{
if (connfactory == null)
{
connfactory = new ConnectionFactory();
connfactory.HostName = HostName;
connfactory.UserName = UserName;
connfactory.Password = Password;
connfactory.Port = Prot;
connfactory.AutomaticRecoveryEnabled = true;//网络故障自动连接恢复
}
}
public MqHelper(string vhost) : this()
{
connfactory.VirtualHost = vhost;
}
public MqHelper(string hostName, string userName, string password, int port, string vhost = "/", bool automaticRecoveryEnabled = true) : this()
{
connfactory.VirtualHost = vhost;
connfactory.AutomaticRecoveryEnabled = automaticRecoveryEnabled;
}
/// <summary>
/// 发送消息
/// </summary>
/// <param name="exchangeName">交换机名称</param>
/// <param name="queueName">队列名称</param>
/// <param name="routingkey">路由名称</param>
public void SenMsg<TEntity>(string exchangeName, TEntity msgEntity, string routingkey = null, string queueName = null, string exchangeType = ExchangeType.Direct, byte deliveryMode = 2, string msgTimeOut = null)
{
using (IConnection conn = connfactory.CreateConnection()) //创建一个连接
{
using (IModel channel = conn.CreateModel()) //创建一个Channel
{
channel.ConfirmSelect();//开启消息确认应答模式 ;当Channel设置成confirm模式时,发布的每一条消息都会获得一个唯一的deliveryTag ;deliveryTag在basicPublish执行的时候加1
try
{
channel.ExchangeDeclare(exchangeName, exchangeType, true, false);
}
catch (Exception)
{
return;//如果交换机创建不成功,可能MQ服务器中已经存在不同类型的同名交换机了,也有可能是Fanout模式下消费端未先启动,请先启动消费端
}
if (exchangeType == ExchangeType.Direct)
{
if (queueName == null || routingkey == null)
{
return; //Direct模式下请先声明QueueName,和routingkey
}
Dictionary<string, object> dic = new Dictionary<string, object>() { { "x-max-length", 50000 } }; //设定这个队列的最大容量为50000条消息
channel.QueueDeclare(queueName, true, false, false, arguments: dic);
channel.QueueBind(queueName, exchangeName, routingkey, arguments: dic);
}
if (exchangeType == ExchangeType.Topic)
{
if (routingkey == null) return; //Topic模式下请先申明routingkey路由
}
/*-------------Return机制:不可达的消息消息监听--------------*/
//这个事件就是用来监听我们一些不可达的消息的内容的:比如某些情况下,如果我们在发送消息时,当前的exchange不存在或者指定的routingkey路由不到,这个时候如果要监听这种不可达的消息,就要使用 return
EventHandler<BasicReturnEventArgs> evreturn = new EventHandler<BasicReturnEventArgs>((o, basic) =>
{
var rc = basic.ReplyCode; //消息失败的code
var rt = basic.ReplyText; //描述返回原因的文本。
var msg = Encoding.UTF8.GetString(basic.Body); //失败消息的内容
//在这里我们可能要对这条不可达消息做处理,比如是否重发这条不可达的消息呀,或者这条消息发送到其他的路由中呀,等等
System.IO.File.AppendAllText("d:/return.txt", "调用了Return;ReplyCode:" + rc + ";ReplyText:" + rt + ";Body:" + msg);
});
channel.BasicReturn += evreturn;
/*-------------Confirm机制:等待确认所有已发布的消息有两种方式----------------*/
//--------方式二:异步
//消息发送成功的时候进入到这个事件:即RabbitMq服务器告诉生产者,我已经成功收到了消息
EventHandler<BasicAckEventArgs> BasicAcks = new EventHandler<BasicAckEventArgs>((o, basic) =>
{
System.IO.File.AppendAllText("d:/ack.txt", "\r\n调用了ack;DeliveryTag:" + basic.DeliveryTag.ToString() + ";Multiple:" + basic.Multiple.ToString() + "时间:" + DateTime.Now.ToString());
});
//消息发送失败的时候进入到这个事件:即RabbitMq服务器告诉生产者,你发送的这条消息我没有成功的投递到Queue中,或者说我没有收到这条消息。
EventHandler<BasicNackEventArgs> BasicNacks = new EventHandler<BasicNackEventArgs>((o, basic) =>
{
//MQ服务器出现了异常,可能会出现Nack的情况
System.IO.File.AppendAllText("d:/nack.txt", "\r\n调用了Nacks;DeliveryTag:" + basic.DeliveryTag.ToString() + ";Multiple:" + basic.Multiple.ToString() + "时间:" + DateTime.Now.ToString());
});
channel.BasicAcks += BasicAcks;
channel.BasicNacks += BasicNacks;
//--------------------------------
IBasicProperties props = channel.CreateBasicProperties();
props.DeliveryMode = deliveryMode; //1:非持久化 2:持续久化 (即:当值为2的时候,我们一个消息发送到服务器上之后,如果消息还没有被消费者消费,服务器重启了之后,这条消息依然存在)
props.Persistent = true;
props.ContentEncoding = "UTF-8"; //注意要大写
if (msgTimeOut != null) { props.Expiration = msgTimeOut; }; //消息过期时间:单位毫秒
props.MessageId = Guid.NewGuid().ToString("N"); //设定这条消息的MessageId(每条消息的MessageId都是唯一的)
string message = Newtonsoft.Json.JsonConvert.SerializeObject(msgEntity);
var msgBody = Encoding.UTF8.GetBytes(message); //发送的消息必须是二进制的
//记住:如果需要EventHandler<BasicReturnEventArgs>事件监听不可达消息的时候,一定要将mandatory设为true
channel.BasicPublish(exchange: exchangeName, routingKey: routingkey, mandatory: true, basicProperties: props, body: msgBody);
/*-------------Confirm机制:等待确认所有已发布的消息有两种方式----------------*/
//--------方式一:同步
//等待确认所有已发布的消息。 //参考资料:https://www.cnblogs.com/refuge/p/10356750.html
//channel.WaitForConfirmsOrDie();//WaitForConfirmsOrDie表示等待已经发送给broker的消息act或者nack之后才会继续执行;即:直到所有信息都发布,如果有任何一个消息触发了Nack则抛出IOException异常
//bool isSendMsgOk = channel.WaitForConfirms(); //WaitForConfirms表示等待已经发送给MQ服务器的消息act或者nack之后才会继续执行。
//if (isSendMsgOk)
//{
// //消息确认已经发送到MQ服务器
//}
//else
//{
// // 进行消息重发
// channel.BasicPublish(exchange: exchangeName, routingKey: routingkey, basicProperties: props, body: msgBody);
//}
//方式一的缺点:
//--------------------------------
}
}
}
}
}
在控制器中调用发送消息类
public class HomeController : Controller
{
public ActionResult Index()
{
new MqHelper("/vhost001").SenMsg("EX.CMS.USER", "你好RabbitMq", "user.info", "CMS.USER", exchangeType: ExchangeType.Direct);
return Content("OK");
}
}
Confirm同步模式:
同步模式分为2种:普通Confirm模式 和 批量Confirm模式
普通Confirm模式
单条confirm模式就是发送一条等待确认一条,使用方式如下:在每发送一条消息就调用channel.waitForConfirms()方法,该方法等待直到自上次调用以来发布的所有消息都已被ack或nack,如果返回false表示消息投递失败,如果返回true表示消息投递成功。注意,如果当前信道没有开启confirm模式
channel.ConfirmSelect();
for (int i = 0; i < 5; i++) //发5条消息到MQ服务器
{
string msg = "你好MQ,这是我的第" + i + "条消息";
var msgBody = Encoding.UTF8.GetBytes(msg);
channel.BasicPublish(exchange: ExchangeName, routingKey: routingKey, basicProperties: props, body: msgBody);
bool isOk = channel.WaitForConfirms(); //每发送一条消息,就等待MQ服务器的ack响应
if (isOk)
{
Console.WriteLine("消息发送成功,MQ服务器确认已经收到消息");
}
else
{
Console.WriteLine("消息发送失败");
}
}
批量Confirm模式
批量confirm模式就是先开启confirm模式,发送多条之后再调用waitForConfirms()方法确认,这样发送多条之后才会等待一次确认消息。相比普通confirm模式,批量极大提升confirm效率,但是问题在于一旦出现confirm返回false或者超时的情况时,客户端如果需要将这一批次的消息全部重发,这会带来明显的重复消息数量,并且,当消息经常丢失时,批量confirm性能应该是不升反降的。
channel.ConfirmSelect();
for (int i = 0; i < 5; i++) //发5条消息到MQ服务器
{
string msg = "你好MQ,这是我的第" + i + "条消息";
var msgBody = Encoding.UTF8.GetBytes(msg);
channel.BasicPublish(exchange: ExchangeName, routingKey: routingKey, basicProperties: props, body: msgBody);
}
//channel.WaitForConfirmsOrDie();//WaitForConfirmsOrDie表示等待已经发送给broker的消息act或者nack之后才会继续执行;即:直到所有信息都发送成功,如果有任何一个消息触发了Nack(即:MQ服务器未确认消息,即:发送失败)则抛出IOException异常
bool isOk = channel.WaitForConfirms(); //等消息全部发送完毕后,等待MQ服务器的ack响应
if (isOk)
{
Console.WriteLine("消息发送成功,MQ服务器确认已经收到消息");
}
else
{
Console.WriteLine("消息发送失败");
}
异步模式
异步模式采用事件监控
异步confirm模式的编程实现最复杂,在.Net中Channel对象提供的BasicAcks,BasicNacks两个回调事件,事件中包含deliveryTag(当前Chanel发出的消息序号),我们需要自己为每一个Channel维护一个unconfirm的消息序号集合,每publish一条数据,集合中元素加1,每回调一次BasicAcks事件方法,unconfirm集合删掉相应的一条(multiple=false)或多条(multiple=true)记录。从程序运行效率上看,这个unconfirm集合最好采用有序集合SortedSet存储结构。实际上,SDK中的waitForConfirms()方法也是通过SortedSet维护消息序号的
异步模式下,如果我们还是发送了向MQ服务器100条消息,可能生产者端并不会收到100个ack消息 ,可能收到1个,或者2个,或N个ack消息,并且这两个ack消息的multiple域都为true,你多次运行程序会发现每次发送回来的ack消息中的deliveryTag域的值并不是一样的,说明MQ服务器批量回传给发送者的ack消息并不是以固定的批量大小回传的;
也就是我们通过信道Channel的waitForConfirmsOrDie方法或者为信道设置监听器都可以保证发送者收到broker回传的ack或者nack消息,那么这两种方式有什么区别呢?从测试中调用waitForConfirmsOrDie方法发送100条消息并且全部收到确认可能需要135ms,而通过监听器的方式仅仅可能需要1ms,说明调用waitForConfirmsOrDie会造成程序的阻塞,通过监听器并不会造成程序的阻塞
static void Main(string[] args)
{
using (IConnection conn = rabbitMqFactory.CreateConnection()) //创建一个连接
{
using (IModel channel = conn.CreateModel()) //创建一个Channel
{
channel.ConfirmSelect();//开启消息确认模式
IBasicProperties props = channel.CreateBasicProperties();
props.Persistent = true;
//消息发送成功的时候进入到这个事件:即RabbitMq服务器告诉生产者,我已经成功收到了消息
EventHandler<BasicAckEventArgs> BasicAcks = new EventHandler<BasicAckEventArgs>((o, basic) =>
{
System.IO.File.AppendAllText("d:/ack.txt", "\r\n调用了ack;DeliveryTag:" + basic.DeliveryTag.ToString() + ";Multiple:" + basic.Multiple.ToString() + "时间:" + DateTime.Now.ToString());
});
//消息发送失败的时候进入到这个事件:即RabbitMq服务器告诉生产者,你发送的这条消息我没有成功的投递到Queue中,或者说我没有收到这条消息。
EventHandler<BasicNackEventArgs> BasicNacks = new EventHandler<BasicNackEventArgs>((o, basic) =>
{
//MQ服务器出现了异常,可能会出现Nack的情况
System.IO.File.AppendAllText("d:/nack.txt", "\r\n调用了Nacks;DeliveryTag:" + basic.DeliveryTag.ToString() + ";Multiple:" + basic.Multiple.ToString() + "时间:" + DateTime.Now.ToString());
});
channel.BasicAcks += BasicAcks;
channel.BasicNacks += BasicNacks;
for (int i = 0; i < 5; i++) //发5条消息到MQ服务器
{
string msg = "你好MQ,这是我的第" + i + "条消息";
var msgBody = Encoding.UTF8.GetBytes(msg);
channel.BasicPublish(exchange: ExchangeName, routingKey: routingKey, basicProperties: props, body: msgBody);
}
Console.ReadKey();
}
}
}
CustomerApp:消费者端的消息确认
这个消费端我就搞了一个控制台应用程序
注:消费者端,和生产者端的消息确认没有任何关系的。都是相互独立的。
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Text;
using System.Threading;
namespace CustomerApp
{
class Program
{
/// <summary>
/// 连接配置
/// </summary>
private static readonly ConnectionFactory rabbitMqFactory = new ConnectionFactory()
{
HostName = "192.168.31.30",
UserName = "admin",
Password = "admin",
Port = 5672,
VirtualHost = "/vhost001",
};
/// <summary>
/// 路由名称
/// </summary>
const string ExchangeName = "EX.CMS.USER";
//队列名称
const string QueueName = "CMS.USER";
const string routingKey = "user.info"; //这里routingkey与消费端的routingkey不保持一致的原因就是要测试生产端的消息不可达,测试不可达所产生的事件调用
static void Main(string[] args)
{
using (IConnection conn = rabbitMqFactory.CreateConnection())
{
using (IModel channel = conn.CreateModel())
{
//channel.ConfirmSelect()消费端是不需要去指定消息的确认应答模式的,消费端本身就是监听
channel.BasicQos(0, 1, false);
channel.ExchangeDeclare(ExchangeName, ExchangeType.Direct, durable: true, autoDelete: false, arguments: null);
channel.QueueDeclare(QueueName, durable: true, autoDelete: false, exclusive: false, arguments: null);
channel.QueueBind(QueueName, ExchangeName, routingKey: routingKey); //交换机与队列进行绑定,并指定了他们的路由
EventingBasicConsumer consumer = new EventingBasicConsumer(channel); //创建一个消费者
consumer.Received += (o, basic) =>//EventHandler<BasicDeliverEventArgs>类型事件
{
try
{
//int aa = 1; int bb = 0; int cc = aa / bb; //模拟异常,这条消息消费失败
var msgBody = basic.Body; //获取消息内容
var a = basic.ConsumerTag;
var c = basic.DeliveryTag;
var d = basic.Redelivered;
var f = basic.RoutingKey;
var e = basic.BasicProperties.Headers;
Console.WriteLine(string.Format("接收时间:{0},消息内容:{1}", DateTime.Now.ToString("HH:mm:ss"), Encoding.UTF8.GetString(msgBody)));
Thread.Sleep(10000);
//手动ACK确认分两种:BasicAck:肯定确认 和 BasicNack:否定确认
channel.BasicAck(deliveryTag: basic.DeliveryTag, multiple: false);//这种情况是消费者告诉RabbitMQ服务器,我已经确认收到了消息
}
catch (Exception)
{
//requeue:被拒绝的是否重新入队列;true:重新进入队列 fasle:抛弃此条消息
//multiple:是否批量.true:将一次性拒绝所有小于deliveryTag的消息
channel.BasicNack(deliveryTag: basic.DeliveryTag, multiple: false, requeue: false);//这种情况是消费者告诉RabbitMQ服务器,因为某种原因我无法立即处理这条消息,这条消息重新回到队列,或者丢弃吧.requeue: false表示丢弃这条消息,为true表示重回队列
}
};
channel.BasicConsume(QueueName, autoAck: false, consumer: consumer);//第二个参数autoAck设为true为自动应答,false为手动ack ;这里一定要将autoAck设置false,告诉MQ服务器,发送消息之后,消息暂时不要删除,等消费者处理完成再说
//while (true)
//{
// BasicGetResult msgResponse = channel.BasicGet(QueueName, autoAck: true);//这个true表示消费完这条数据是否删除,true表示删除,false表示不删除
// if (msgResponse != null)
// {
// var msgBody = Encoding.UTF8.GetString(msgResponse.Body);
// Console.WriteLine(string.Format("接收时间:{0},消息内容:{1}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), msgBody));
// }
//}
Console.ReadKey();
}
}
}
}
}