NetCore RabbitMQ 的消息確認機制 NetCore RabbitMQ Topics 通配符模式

十年河東,十年河西,莫欺少年窮

學無止境,精益求精

上一節介紹了RabbitMQ定向模式,本篇介紹Rabbitmq 的消息確認機制

我的系列博客:

NetCore RabbitMQ Topics 通配符模式

NetCore RabbitMQ ,Routing定向模式

NetCore RabbitMQ 發佈訂閱模式,消息廣播

RabbitMQ的六種工作模式

NetCore RabbitMQ 簡介及兔子生產者、消費者 【簡單模式,work工作模式,競爭消費】

windows環境下,RabbitMQ 安裝教程

kafka、Rabbitmq、EasyNetQ NetCore 源碼下載 

在一些場合,如支付時每一條消息都必須保證成功的被處理。

AMQP是金融級的消息隊列協議,有很高的可靠性,這裏介紹在使用RabbitMQ時怎麼保證消息被成功處理的。

消息確認可以分爲兩種:

一種是生產者發送消息到Broke時,Broker給生產者發送確認回執,用於告訴生產者消息已被成功發送到Broker;

一種是消費者接收到Broker發送的消息時,消費者給Broker發送確認回執,用於通知消息已成功被消費者接收。

生產者端消息確認機制

生產者端消息確認機制分爲兩種,一種是基於事務機制,另一種是基於Confrim確認機制。事務機制佔用資源較多,會拉低生產效率,因此,事務模式在市場上用的比較少。

事務機制【資源佔用高,效率低】

事務機制類似於數據庫事務,要先開啓事務,發完消息後,提交事務,發生異常時回滾事務。具體展現在C#代碼如下:

 channel.TxSelect(); //開啓事務模式

 channel.TxCommit();//提交事務

 channel.TxRollback();//異常時,回滾事務

使用事務機制,我們首先要通過txSelect方法開啓事務,然後發佈消息給broker服務器了,如果txCommit提交成功了,則說明消息成功被broker接收了;

如果在txCommit執行之前broker異常崩潰或者由於其他原因拋出異常,這個時候我們可以捕獲異常,通過txRollback回滾事務。看一個事務機制的簡單實現:

using RabbitMQ.Client;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace RabbitMqProducer
{
    class Program
    {
        static void Main(string[] args)
        {
            ConnectionFactory factory = new ConnectionFactory();
            factory.HostName = "127.0.0.1"; //主機名
            factory.UserName = "guest";//使用的用戶
            factory.Password = "guest";//用戶密碼
            factory.Port = 5672;//端口號
            factory.VirtualHost = "/"; //虛擬主機
            factory.MaxMessageSize = 1024; //消息最大字節數
            using (var connection = factory.CreateConnection())
            {
                //rabbitMQ 基於信道進行通信,因此,我們需要實例化信道Channel
                using (var channel = connection.CreateModel())
                {
                    string Ename = "MyExChange";
                    channel.ExchangeDeclare(Ename, ExchangeType.Direct, false, false, null);
                    //聲明廣播的隊列 
                    string QnameName = "MyQueue";
                    channel.QueueDeclare(QnameName, false, false, false, null);

                    string routingKey = "MyroutingKey"; // 
                                                        // 
                    channel.QueueBind(QnameName, Ename, routingKey);

                    var messages = "Hello,RabbitMQ的事務方式"; //
                    try
                    {
                        channel.TxSelect(); //開啓事務模式
                        //發送消息
                        for (int i = 0; i < 10; i++)
                        { 
                            channel.BasicPublish(Ename, routingKey, null, Encoding.UTF8.GetBytes(messages + "_" + i)); //  
                        }
                        channel.TxCommit();//提交事務
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.ToString());
                        channel.TxRollback();//異常時,回滾事務
                    }

                }
            }
            Console.Read();
        }
    }
}

上述代碼執行後

 

 

 如果我們將上述代碼:channel.BasicPublish(Ename, routingKey, null, Encoding.UTF8.GetBytes(messages + "_" + i)); // 中的routingKey修改爲空字符串,如下:

channel.BasicPublish(Ename, "", null, Encoding.UTF8.GetBytes(messages + "_" + i)); // 

再次運行發現,消息並不能發送到隊列中,程序也不會報異常。也就是說,事務提交了,但消息並沒發出去。

因此:雖說執行了事務提交,程序也沒報異常,但消息不一定會發出去

Confirm確認機制【推薦模式】

C#的RabbitMQ API中,有三個與Confirm相關的方法:ConfirmSelect(),WaitForConfirms(),WaitForConfirmOrDie()

channel.ConfirmSelect() 表示開啓Confirm模式;
channel.WaitForConfirms() 等待所有消息確認,如果所有的消息都被服務端成功接收返回true,只要有一條沒有被成功接收就返回false。 channel.WaitForConfirmsOrDie() 和WaitForConfirms作用類似,也是等待所有消息確認,區別在於該方法沒有返回值(Void),如果有任意一條消息沒有被成功接收,該方法會立即拋出一個OperationInterrupedException類型異常。

看一個Confirm模式的簡單實現:

using RabbitMQ.Client;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace RabbitMqProducer
{
    class Program
    {
        static void Main(string[] args)
        {
            ConnectionFactory factory = new ConnectionFactory();
            factory.HostName = "127.0.0.1"; //主機名
            factory.UserName = "guest";//使用的用戶
            factory.Password = "guest";//用戶密碼
            factory.Port = 5672;//端口號
            factory.VirtualHost = "/"; //虛擬主機
            factory.MaxMessageSize = 1024; //消息最大字節數
            using (var connection = factory.CreateConnection())
            {
                //rabbitMQ 基於信道進行通信,因此,我們需要實例化信道Channel
                using (var channel = connection.CreateModel())
                {
                    string Ename = "MyExChange";
                    channel.ExchangeDeclare(Ename, ExchangeType.Direct, false, false, null);
                    //聲明廣播的隊列 
                    string QnameName = "MyQueue";
                    channel.QueueDeclare(QnameName, false, false, false, null);

                    string routingKey = "MyroutingKey"; // 
                                                        // 
                    channel.QueueBind(QnameName, Ename, routingKey);

                    var messages = "Hello,RabbitMQ的事務方式"; //

                    channel.ConfirmSelect(); // 啓用服務器確認機制方式
                                             // 
                    for (int i = 0; i < 10; i++)
                    { 
                        channel.BasicPublish(Ename, routingKey, null, Encoding.UTF8.GetBytes(messages + "_" + i));  //發送消息
                    }
                    if (channel.WaitForConfirms())
                    {
                        Console.WriteLine("消息發送成功");
                    }
                    else
                    {
                        //重發 或者 寫具體的處理邏輯
                        Console.WriteLine("消息發送失敗");
                    }
                }
            }
            Console.Read();
        }
    }
}

 這裏需要說明的是,WaitForConfirms是指等待所有消息確認,如果你在調試過程中,將發送消息刻意循環三次,在執行WaitForConfirms時,返回值依舊是True,因此三次發送均成功了。

 我在網上看到還有一種寫法是這樣的,如下:

                    for (int i = 0; i < 10; i++)
                    {
                        channel.ConfirmSelect(); // 啓用服務器確認機制方式
                        channel.BasicPublish(Ename, routingKey, null, Encoding.UTF8.GetBytes(messages + "_" + i));  //發送消息
                        if (channel.WaitForConfirms())
                        {
                            Console.WriteLine("消息發送成功");
                        }
                        else
                        {
                            //重發 或者 寫具體的處理邏輯
                            Console.WriteLine("消息發送失敗");
                        }
                    }

每發一次消息,確認一次,這種寫法無疑會浪費資源。大家有何看法,歡迎評論。~_~

同理

如果我們將上述代碼:channel.BasicPublish(Ename, routingKey, null, Encoding.UTF8.GetBytes(messages + "_" + i)); // 中的routingKey修改爲空字符串,如下:

channel.BasicPublish(Ename, "", null, Encoding.UTF8.GetBytes(messages + "_" + i)); // 

再次運行發現,消息並不能發送到隊列中,程序也不會報異常。但WaitForConfirms依舊返回True

因此,這種機制還事務模式一樣,都不能完全保證消息發送到 隊列。

消費者端消息確認機制(自動確認和顯示確認)

 從Broke發送到消費者時,RabbitMQ提供了兩種消息確認的方式:自動確認和顯示確認。

1 自動確認

  自動確認:當RabbbitMQ將消息發送給消費者後,消費者端接收到消息後,不等待消息處理結束,立即自動回送一個確認回執。自動確認的用法十分簡單,設置消費方法的參數autoAck爲true即可,如下:

channel.BasicConsume(Qname, true, consumer);//開啓自動確認

        注意:Broker會在接收到確認回執時刪除消息,如果消費者接收到消息並返回了確認回執,然後這個消費者在處理消息時掛了,那麼這條消息就再也找不回來了。

消費者代碼如下

using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace RabbitMQConsumer_2
{
    class Program
    {
        static void Main(string[] args)
        {
            ConnectionFactory factory = new ConnectionFactory();
            factory.HostName = "127.0.0.1"; //主機名
            factory.UserName = "guest";//使用的用戶
            factory.Password = "guest";//用戶密碼
            factory.Port = 5672;//端口號
            factory.VirtualHost = "/"; //虛擬主機
            factory.MaxMessageSize = 1024; //消息最大字節數 
                                           //創建連接
            var connection = factory.CreateConnection();
            //創建通道
            var channel = connection.CreateModel();

            //事件基本消費者
            EventingBasicConsumer consumer = new EventingBasicConsumer(channel);
            //接收到消息事件
            consumer.Received += (ch, ea) =>
            {
                var message = Encoding.UTF8.GetString(ea.Body.ToArray());
                Console.WriteLine($"消費者收到消息: {message}");
                //確認該消息已被消費
                channel.BasicAck(ea.DeliveryTag, false); Thread.Sleep(100);
            };
            //啓動消費者 
            string Qname = "MyQueue";
            channel.BasicConsume(Qname, true, consumer);//開啓自動確認
            Console.WriteLine("消費者已啓動");
            Console.ReadKey();
            channel.Dispose();
            connection.Close();
        }
    }
}
View Code

2 顯示確認

自動確認可能會出現消息丟失的問題,我們不免會想到:Broker收到回執後才刪除消息,如果可以讓消費者在接收消息時不立即返回確認回執,等到消息處理完成後(或者完成一部分的邏輯)再返回確認回執,這樣就保證消費端不會丟失消息了!這正是顯式確認的思路。使用顯示確認也比較簡單,首先將Resume方法的參數autoAck設置爲false,然後在消費端使用代碼 channel.BasicAck()/BasicReject()等方法 來確認和拒絕消息。看一個栗子:

生產者代碼:

using RabbitMQ.Client;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace RabbitMqProducer
{
    class Program
    {
        static void Main(string[] args)
        {
            ConnectionFactory factory = new ConnectionFactory();
            factory.HostName = "127.0.0.1"; //主機名
            factory.UserName = "guest";//使用的用戶
            factory.Password = "guest";//用戶密碼
            factory.Port = 5672;//端口號
            factory.VirtualHost = "/"; //虛擬主機
            factory.MaxMessageSize = 1024; //消息最大字節數
            using (var connection = factory.CreateConnection())
            {
                //rabbitMQ 基於信道進行通信,因此,我們需要實例化信道Channel
                using (var channel = connection.CreateModel())
                {
                    string Ename = "MyExChange";
                    channel.ExchangeDeclare(Ename, ExchangeType.Direct, false, false, null);
                    //聲明廣播的隊列 
                    string QnameName = "MyQueue";
                    channel.QueueDeclare(QnameName, false, false, false, null);

                    string routingKey = "MyroutingKey"; // 
                                                        // 
                    channel.QueueBind(QnameName, Ename, routingKey);

                    var messages = "MyHello,RabbitMQ"; // 
                    // 
                    for (int i = 0; i < 10; i++)
                    {
                        channel.BasicPublish(Ename, routingKey, null, Encoding.UTF8.GetBytes(messages + "_" + i));  //發送消息 
                    }
                   
                }
            }
            Console.Read();
        }
    }
}
View Code

消費者代碼:

using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace RabbitMQConsumer_2
{
    class Program
    {
        static void Main(string[] args)
        {
            ConnectionFactory factory = new ConnectionFactory();
            factory.HostName = "127.0.0.1"; //主機名
            factory.UserName = "guest";//使用的用戶
            factory.Password = "guest";//用戶密碼
            factory.Port = 5672;//端口號
            factory.VirtualHost = "/"; //虛擬主機
            factory.MaxMessageSize = 1024; //消息最大字節數 
                                           //創建連接
            var connection = factory.CreateConnection();
            //創建通道
            var channel = connection.CreateModel();

            //事件基本消費者
            EventingBasicConsumer consumer = new EventingBasicConsumer(channel);
            //接收到消息事件
            consumer.Received += (ch, ea) =>
            {
                var message = Encoding.UTF8.GetString(ea.Body.ToArray());

                //確認該消息已被消費
                if (message.StartsWith("Hello"))
                {
                    Console.WriteLine($"消費者收到消息: {message}");
                    channel.BasicAck(ea.DeliveryTag, false); 
                } 
                else
                {
                    Console.WriteLine($"消費者拒絕接收的消息: {message}");
                    //拒絕接收
                    channel.BasicReject(ea.DeliveryTag, false); 
                }
            };
            //啓動消費者 
            string Qname = "MyQueue";
            channel.BasicConsume(Qname, false, consumer);//開啓自動確認
            Console.WriteLine("消費者已啓動");
            Console.ReadKey();
            channel.Dispose();
            connection.Close();
        }
    }
}
View Code

注意:顯示確認時,自動確認要關閉。

                //確認該消息已被消費
                if (message.StartsWith("Hello"))
                {
                    Console.WriteLine($"消費者收到消息: {message}");
                    //deliveryTag 參數分發的標記
                    //multiple  是否確認多條
                    //void BasicAck(ulong deliveryTag, bool multiple);
                    channel.BasicAck(ea.DeliveryTag, false); 
                } 
                else
                {
                    Console.WriteLine($"消費者拒絕接收的消息: {message}");
                    //deliveryTag 參數分發的標記
                    // requeue false 時,拒絕的消息會被直接刪除  true 拒絕的消息會被重新放入隊列中
                    //void BasicReject(ulong deliveryTag, bool requeue);
                    //拒絕接收  
                    channel.BasicReject(ea.DeliveryTag, false); 
                }

介紹一下代碼中的兩個方法: channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false); 方法用於確認消息,deliveryTag參數是分發的標記,multiple表示是否確認多條。 channel.BasicReject(deliveryTag: ea.DeliveryTag, requeue: false); 方法用於拒絕消息,deliveryTag也是指分發的標記,requeue表示消息被拒絕後是否重新放回queue中,true表示放回queue中,false表示直接丟棄。

  運行這兩個應用程序,通過生產者發送兩條消息,效果如下:

 

 一些意外的情況:使用顯式確認時,如果消費者處理完消息不發送確認回執,那麼消息不會被刪除,消息的狀態一直是Unacked,這條消息也不會再發送給其他消費者。如果一個消費者在處理消息時尚未發送確認回執的情況下掛掉了,那麼消息會被重新放入隊列(狀態從Unacked變成Ready),有其他消費者存時,消息會發送給其他消費者。

例如,我們將上述消費者代碼中的回執部分註釋掉,如下

 

 再次生產消息後,運行消費者端代碼,此時,消息的狀態爲:Unacked

 關閉消費者端調試後,消息狀態又變成了 Ready

@天才臥龍的波爾卡

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章