8.【RabbitMQ實戰】- 發佈確認高級

在生產環境中由於一些不明原因,導致 rabbitmq 重啓,在 RabbitMQ 重啓期間生產者消息投遞失敗,導致消息丟失,需要手動處理和恢復。如何才能進行 RabbitMQ 的消息可靠投遞呢? 特別是在這樣比較極端的情況,RabbitMQ 集羣不可用的時候,無法投遞的消息該如何處理呢?

消息回退(mandatory = true)

確認機制方案

image.png

如圖所示生產者將消息發給broker,以下考慮兩個情況

  1. 交換機不存在或交換機命名寫錯了,即交換機收不到消息如何告訴生產者(.Net版本此種情況不存在,交換機寫錯了會直接報錯)
  2. RoutingKey寫錯了,隊列收不到消息如何告訴生產者,即消息回退(以下演示這種情況)

代碼架構圖

image.png

RabbitmqUntils配置代碼新增方法GetConfirmAdvancedQueue()

        /// <summary>
        /// 發佈確認高級
        /// </summary>
        /// <returns></returns>
        public static IModel GetConfirmAdvancedQueue()
        {
            /*
                1. 聲身confirm.exchange
                2. 聲明confirm.queue
                3. 聲明confirm.routingkey
                4. 綁定隊列與交換機
             */
         
            var channel = RabbitmqUntils.GetChannel();
            channel.ExchangeDeclare(Confirm_Exchange,"direct",false,false,null);
            channel.QueueDeclare(Confirm_Queue, false, false, false, null);
            channel.QueueBind(Confirm_Queue,Confirm_Exchange,Confirm_Routingkey);
            return channel;
        }

生產者代碼

在僅開啓了生產者確認機制的情況下,交換機接收到消息後,會直接給消息生產者發送確認消息
果發現該消息不可路由,那麼消息會被直接丟棄,此時生產者是不知道消息被丟棄這個事件的。那麼如何
讓無法被路由的消息幫我想辦法處理一下?最起碼通知我一聲,好自己處理。通過設置mandatory參
數可以在當消息傳遞過程中不可達目的地時將消息返回給生產者

image.png

using rabbitmq.common;

using RabbitMQ.Client;

using System.Text;

namespace PublishConfirm.Producer
{
    public class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("請輸入要發送的消息:");
            string message = Console.ReadLine();

            if (string.IsNullOrEmpty(message))
            {
                while (true)
                {
                    Console.WriteLine("請輸入要發送的消息: {0}", message);
                }
            }
            else
            {
                using var channel = RabbitmqUntils.GetConfirmAdvancedQueue();
                while (true)
                {
                    SendMsg(message, channel);
                    message = Console.ReadLine();
                    
                }
            }
            Console.ReadKey();
        }

        public static void SendMsg(string msg,IModel channel)
        {
            var body = Encoding.UTF8.GetBytes(msg);
            channel.ConfirmSelect();// 開啓發布確認

            //演示Routingkey配置錯誤
            //如果發佈了帶有“mandatory” = true 標誌集的消息,但無法傳遞,代理將其返回給發送客戶端( channel.BasicReturn)。
            channel.BasicPublish(RabbitmqUntils.Confirm_Exchange, RabbitmqUntils.Confirm_Routingkey+"111", mandatory:true, null, body);
            // 監聽確認的消息
            channel.BasicAcks += (sender, e) =>
            {
                Console.WriteLine($"交換機已收到:{e.DeliveryTag}");
            };

            //監聽回退的消息
            channel.BasicReturn += ((sender, e) =>
            {
                var body = e.Body.ToArray();
                var msg = System.Text.Encoding.UTF8.GetString(body);
                Console.WriteLine($"監聽回退的消息{msg};RoutingKey:{e.RoutingKey};退回原因:{e.ReplyText}");
            });
        }

        
    }
}

測試效果

image.png

備份交換機

有了mandatory參數和回退消息,我們獲得了對無法投遞消息的感知能力,有機會在生產者的消息無法被投遞時發現並處理。但有時候,我們並不知道該如何處理這些無法路由的消息,最多打個日誌,然後觸發報警,再來手動處理。而通過日誌來處理這些無法路由的消息是很不優雅的做法,特別是當生產者所在的服務有多臺機器的時候,手動複製日誌會更加麻煩而且容易出錯。而且設置mandatory參數會增加生產者的複雜性,需要添加處理這些被退回的消息的邏輯。如果既不想丟失消息,又不想增加生產者的複雜性,該怎麼做呢?前面在設置死信隊列中,我們提到,可以爲隊列設置死信交換機來存儲那些處理失敗的消息,可是這些不可路由消息根本沒有機會進入到隊列,因此無法使用死信隊列來保存消息。在RabbitMQ中,有一種備份交換機的機制存在,可以很好的應對這個問題。什麼是備份交換機呢?備份交換機可以理解爲RabbitMQ中交換機的“備胎”,當我們爲某一個交換機聲明一個對應的備份交換機時,就是爲它創建一個備胎,當交換機接收到一條不可路由消息時,將會把這條消息轉發到備份交換機中,由備份交換機來進行轉發和處理,通常備份交換機的類型爲Fanout,這樣就能把所有消息都投遞到與其綁定的隊列中,然後我們在備份交換機下綁定一個隊列,這樣所有那些原交換機無法被路由的消息,就會都進入這個隊列了。當然,我們還可以建立一個報警隊列,用獨立的消費者來進行監測和報警。

代碼架構圖

image.png

RabbitmqUntils.GetConfirmAdvancedQueue()方法增加代碼

image.png

        /// <summary>
        /// 發佈確認高級
        /// </summary>
        /// <returns></returns>
        public static IModel GetConfirmAdvancedQueue()
        {
            /*
                1. 聲明confirm.exchange   type = direct
                2. 聲明confirm.queue
                3. 聲明confirm.routingkey
                4. 綁定隊列與交換機
             */

            var channel = RabbitmqUntils.GetChannel();

            var arguments = new Dictionary<string, object>();
            arguments.Add("alternate-exchange",Back_Exchange);

            channel.ExchangeDeclare(Confirm_Exchange,"direct",true,false, arguments);
            channel.QueueDeclare(Confirm_Queue, false, false, false, null);
            channel.QueueBind(Confirm_Queue,Confirm_Exchange,Confirm_Routingkey);

            /*
               1. 聲明backup.exchange type = fanout
               2. 聲明backup.queue,warning.queue
               4. 綁定隊列與交換機
               5. 配置確認交換機(Confirm_Exchange)轉發到備份交換機(Back_Exchange)
            */
            channel.ExchangeDeclare(Back_Exchange, "fanout", true, false, null);
            channel.QueueDeclare(Back_Queue,false,false, false, null);
            channel.QueueDeclare(Warning_Queue,false,false, false, null);
            channel.QueueBind(Back_Queue, Back_Exchange, "");
            channel.QueueBind(Warning_Queue, Back_Exchange, "");

            return channel;
        }

生產者代碼

image.png

using rabbitmq.common;

using RabbitMQ.Client;

using System.Text;

namespace PublishConfirm.Producer
{
    public class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("請輸入要發送的消息:");
            string message = Console.ReadLine();

            if (string.IsNullOrEmpty(message))
            {
                while (true)
                {
                    Console.WriteLine("請輸入要發送的消息: {0}", message);
                }
            }
            else
            {
                var channel = RabbitmqUntils.GetConfirmAdvancedQueue();
                while (true)
                {
                    SendMsg(message, channel);
                    message = Console.ReadLine();
                    
                }
            }
            Console.ReadKey();
        }

        public static void SendMsg(string msg,IModel channel)
        {
            var body1 = Encoding.UTF8.GetBytes("路由正常_"+msg);
            var body2 = Encoding.UTF8.GetBytes("路由不正常_" + msg);
            channel.ConfirmSelect();// 開啓發布確認
            channel.BasicPublish(RabbitmqUntils.Confirm_Exchange, RabbitmqUntils.Confirm_Routingkey, mandatory: true, null, body1);

            //演示Routingkey配置錯誤
            //如果發佈了帶有“mandatory” = true 標誌集的消息,但無法傳遞,代理將其返回給發送客戶端( channel.BasicReturn)。
            channel.BasicPublish(RabbitmqUntils.Confirm_Exchange, RabbitmqUntils.Confirm_Routingkey+"111", mandatory:true, null, body2);
            // 監聽確認的消息
            channel.BasicAcks += (sender, e) =>
            {
                Console.WriteLine($"交換機已收到:{e.DeliveryTag}");
            };

            //監聽回退的消息
            channel.BasicReturn += ((sender, e) =>
            {
                var body = e.Body.ToArray();
                var msg = System.Text.Encoding.UTF8.GetString(body);
                Console.WriteLine($"監聽回退的消息{msg};RoutingKey:{e.RoutingKey};退回原因:{e.ReplyText}");
            });
        }  
    }
}

消費者者代碼

using rabbitmq.common;

using RabbitMQ.Client;
using RabbitMQ.Client.Events;

using System.Text;

namespace PublishConfirm.Consumer
{
    public class Program
    {
        static void Main(string[] args)
        {
            if (args[0] == "client1")
            {
                ConfirmConsumer();
            } else if (args[0] == "client2")
            {
                BackupConsumer();
            }
            else if (args[0] == "client3")
            {
                WarningConsumer();
            }
            Console.ReadKey();
        }


        public static void ConfirmConsumer()
        {
            Console.WriteLine("ConfirmConsumer開始接受消息:");
            var channel = RabbitmqUntils.GetConfirmAdvancedQueue();
            var consumer = new EventingBasicConsumer(channel);
            channel.BasicConsume(RabbitmqUntils.Confirm_Queue, false, consumer);
            consumer.Received += ((sender, e) =>
            {
                var body = e.Body.ToArray();
                var message = Encoding.UTF8.GetString(body);
                Console.WriteLine($"ConfirmConsumer{DateTime.Now} 接收消息:{message}");
                channel.BasicAck(e.DeliveryTag, false);
            });
        }

        public static void BackupConsumer()
        {
            Console.WriteLine("BackupConsumer開始接受消息:");
            var channel = RabbitmqUntils.GetConfirmAdvancedQueue();
            var consumer = new EventingBasicConsumer(channel);
            channel.BasicConsume(RabbitmqUntils.Back_Queue, false, consumer);
            consumer.Received += ((sender, e) =>
            {
                var body = e.Body.ToArray();
                var message = Encoding.UTF8.GetString(body);
                Console.WriteLine($"BackupConsumer{DateTime.Now} 接收消息:{message}");
                channel.BasicAck(e.DeliveryTag, false);
            });
        }
        public static void WarningConsumer()
        {
            Console.WriteLine("WarningConsumer開始接受消息:");
            var channel = RabbitmqUntils.GetConfirmAdvancedQueue();
            var consumer = new EventingBasicConsumer(channel);
            channel.BasicConsume(RabbitmqUntils.Warning_Queue, false, consumer);
            consumer.Received += ((sender, e) =>
            {
                var body = e.Body.ToArray();
                var message = Encoding.UTF8.GetString(body);
                Console.WriteLine($"WarningConsumer{DateTime.Now} 接收消息:{message}");
                channel.BasicAck(e.DeliveryTag, false);
            });
        }
    }
}

測試效果

注意:測試時我們需要把原來的**confirm.queue**刪除,因爲我們修改了屬性,否則會報錯
image.png
mandatory參數與備份交換機可以一起使用的時候,如果兩者同時開啓,經過上面結果顯示答案是備份交換機優先級高。

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