Net中使用 RabbitMq | Confirm确认消息

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Fanbin168/article/details/89357186

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();
                }
            }
        }
    }
}

猜你喜欢

转载自blog.csdn.net/Fanbin168/article/details/89357186