十年河東,十年河西,莫欺少年窮
學無止境,精益求精
上一節介紹了RabbitMQ定向模式,本篇介紹Rabbitmq 的消息確認機制
我的系列博客:
NetCore RabbitMQ 簡介及兔子生產者、消費者 【簡單模式,work工作模式,競爭消費】
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(); } } }
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(); } } }
消費者代碼:
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(); } } }
注意:顯示確認時,自動確認要關閉。
//確認該消息已被消費 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
@天才臥龍的波爾卡