本篇介紹一下RabbitMQ中的消費模式,在前邊的所有栗子中我們採用的消費者都是EventingBasicConsumer,其實RabbitMQ中還有其他兩種消費模式:BasicGet和QueueBaicConsumer,下邊介紹RabiitMQ的消費模式,及使用它們時需要注意的一些問題。
1 RabbitMQ的消費模式
0 準備工作
使用Web管理工具新增exchange、queue並繫結,bindingKey為“mykey”,如下所示:
生產者程式碼如下:
static void Main(string[] args) { var factory = new ConnectionFactory() { //rabbitmq-server所在裝置ip,這裡就是本機 HostName = "127.0.0.1", UserName = "wyy",//使用者名稱 Password = "123321"//密碼 }; //建立連線connection using (var connection = factory.CreateConnection()) { //建立通道channel using (var channel = connection.CreateModel()) { Console.WriteLine("生產者準備就緒...."); string message = ""; //在控制檯輸入訊息,按enter鍵傳送訊息 while (!message.Equals("quit", StringComparison.CurrentCultureIgnoreCase)) { message = Console.ReadLine(); var body = Encoding.UTF8.GetBytes(message); channel.BasicPublish(exchange: "myexchange", routingKey: "mykey", basicProperties: null, body: body); Console.WriteLine($"【{message}】傳送到Broke成功!"); } } } Console.ReadKey(); }
1 EventingBasicConsumer介紹
EventingBasicConsumer是釋出/訂閱模式的消費者,即只要訂閱的queue中有了新訊息,Broker就會立即把訊息推送給消費者,這種模式可以保證訊息及時地被消費者接收到。EventingBasicConsumer是長連線的:只需要建立一個Connection,然後在Connection的基礎上建立通道channel,訊息的傳送都是通過channel來執行的,這樣可以減少Connection的建立,比較節省資源。前邊我們已經使用了很多次EventingBaiscConsumer,這裡簡單展示一下使用的方式,註釋比較詳細,就不多介紹了。
static void Main(string[] args) { var factory = new ConnectionFactory() { //rabbitmq-server所在裝置ip,這裡就是本機 HostName = "127.0.0.1", UserName = "wyy",//使用者名稱 Password = "123321"//密碼 }; using (var connection = factory.CreateConnection()) { using (var channel = connection.CreateModel()) { #region EventingBasicConsumer //定義一個EventingBasicConsumer消費者 var consumer = new EventingBasicConsumer(channel); //接收到訊息時觸發的事件 consumer.Received += (model, ea) => { Console.WriteLine(Encoding.UTF8.GetString(ea.Body)); }; Console.WriteLine("消費者準備就緒...."); //呼叫消費方法 queue指定消費的佇列,autoAck指定是否自動確認,consumer就是消費者物件 channel.BasicConsume(queue: "myqueue", autoAck: true, consumer: consumer); Console.ReadKey(); #endregion } } }
執行程式,結果如下,只要我們在生產者端傳送一條訊息到Broker,Broker就會立即推送訊息到消費者。
2 BasicGet方法介紹
我們知道使用EventingBasicConsumer可以讓消費者最及時地獲取到訊息,使用EventingBasicConsumer模式時消費者在被動的接收訊息,即訊息是推送過來的,Broker是主動的一方。那麼能不能讓消費者作為主動的一方,消費者什麼時候想要訊息了,就自己傳送一個請求去找Broker要?答案使用Get方式。Get方式是短連線的,消費者每次想要訊息的時候,首先建立一個Connection,傳送一次請求,Broker接收到請求後,響應一條訊息給消費者,然後斷開連線。RabbitMQ中Get方式和HTTP的請求響應流程基本一樣,Get方式的實時性比較差,也比較耗費資源。我們看一個Get方式的栗子:
static void Main(string[] args) { var factory = new ConnectionFactory() { //rabbitmq-server所在裝置ip,這裡就是本機 HostName = "127.0.0.1", UserName = "wyy",//使用者名稱 Password = "123321"//密碼 }; using (var connection = factory.CreateConnection()) { using (var channel = connection.CreateModel()) { #region BasicGet //通過BasicGet獲取訊息,開啟自動確認 BasicGetResult result = channel.BasicGet(queue:"myqueue",autoAck:true); Console.WriteLine($"接收到訊息【{Encoding.UTF8.GetString(result.Body)}】"); //列印exchange和routingKey Console.WriteLine($"exchange:{result.Exchange},routingKey:{result.RoutingKey}"); Console.ReadLine(); #endregion } } }
執行生成者和消費者程式,生產者傳送三條訊息,而消費者只獲取了一條訊息,這是因為channel.BasicGet()一次只獲取一條訊息,獲取到訊息後就把連線斷開了。
補充:RabbitMQ還有一種消費者QueueBaicConsumer,用法和Get方式類似,QueueBaicConsumer在官方API中標記已過時,這裡不再介紹,有興趣的小夥伴可以自己研究下。
2 Qos介紹
在介紹Qos(服務質量)前我們先看一下使用EventingBasicConsumer的一個坑,使用程式碼演示一下,簡單修改一下上邊栗子的程式碼
生產者程式碼如下,這裡生產者傳送了100條消費到Broker
static void Main(string[] args) { var factory = new ConnectionFactory() { //rabbitmq-server所在裝置ip,這裡就是本機 HostName = "127.0.0.1", UserName = "wyy",//使用者名稱 Password = "123321"//密碼 }; //建立連線connection using (var connection = factory.CreateConnection()) { //建立通道channel using (var channel = connection.CreateModel()) { Console.WriteLine("生產者準備就緒...."); #region 新增100條資料 for (int i = 0; i < 100; i++) { channel.BasicPublish(exchange: "myexchange", routingKey: "mykey", basicProperties: null, body: Encoding.UTF8.GetBytes($"第{i}條訊息")); } #endregion } } Console.ReadKey(); }
消費端程式碼如下,消費端採用的是自動確認(autoAck=true),即Broker把訊息傳送給消費者就會確認成功,不關心訊息有沒有處理完成,假設每條訊息處理需要5s
static void Main(string[] args) { var factory = new ConnectionFactory() { //rabbitmq-server所在裝置ip,這裡就是本機 HostName = "127.0.0.1", UserName = "wyy",//使用者名稱 Password = "123321"//密碼 }; using (var connection = factory.CreateConnection()) { using (var channel = connection.CreateModel()) { #region EventingBasicConsumer //定義消費者 var consumer = new EventingBasicConsumer(channel); //接收到訊息時執行的任務 consumer.Received += (model, ea) => { Thread.Sleep(1000 * 5); Console.WriteLine($"處理訊息【{Encoding.UTF8.GetString(ea.Body)}】完成"); }; Console.WriteLine("消費者準備就緒...."); //處理訊息 channel.BasicConsume(queue: "myqueue", autoAck: true, consumer: consumer); Console.ReadKey(); #endregion } } }
我們先執行生產者程式,執行完成後發現queue中有了100條ready狀態的訊息,表示訊息成功傳送到了佇列
接著我們執行消費者,消費者執行後,Broker會把訊息一股腦傳送過去,通過Web管理介面我們看到queue中已經沒有訊息了,如下:
我們再看一下消費者的執行情況,發現消費者僅僅處理了4條訊息,還有96條訊息沒有處理,這就是說消費者沒有處理完訊息,但是queue中的訊息都已經刪除了。如果這時消費者掛掉了,所有未處理的訊息都會丟失,在某些場合中,丟失資料的後果是十分嚴重的。
對於上邊的問題,我們可能會想到使用顯示確認來保證訊息不會丟失:將BasicConsume方法的autoAck設定為false,然後處理一條訊息後手動確認一下,這樣的話已處理的訊息在接收到確認回執時被刪除,未處理的訊息以Unacked狀態存放在queue中。如果消費者掛了,Unacked狀態的訊息會自動重新變成Ready狀態,如此一來就不用擔心訊息丟失了,修改消費者程式碼如下:
static void Main(string[] args) { var factory = new ConnectionFactory() { //rabbitmq-server所在裝置ip,這裡就是本機 HostName = "127.0.0.1", UserName = "wyy",//使用者名稱 Password = "123321"//密碼 }; using (var connection = factory.CreateConnection()) { using (var channel = connection.CreateModel()) { #region EventingBasicConsumer //定義消費者 var consumer = new EventingBasicConsumer(channel); //接收到訊息時執行的任務 consumer.Received += (model, ea) => { Thread.Sleep(1000 * 5); //處理完成,手動確認 channel.BasicAck(ea.DeliveryTag, false); Console.WriteLine($"處理訊息【{Encoding.UTF8.GetString(ea.Body)}】完成"); }; Console.WriteLine("消費者準備就緒...."); //處理訊息 channel.BasicConsume(queue: "myqueue", autoAck: false, consumer: consumer); Console.ReadKey(); #endregion } } }
重新執行生產者,然後執行消費者,Web管理其中看到結果如下:在執行消費者時,訊息會一股腦的傳送給消費者,然後狀態都變成Unacked,消費者執行完一條資料手動確認後,這條訊息從queue中刪除。當消費者掛了(我們可以直接把消費者關掉來模擬掛掉的情況),沒有處理的訊息會自動從Unacked狀態變成Ready狀態,不用擔心訊息丟失了!開啟Web管理介面看到狀態如下:
通過顯式確認的方式可以解決訊息丟失的問題,但這種方式也存在一些問題:①當訊息有十萬,百萬條時,一股腦的把訊息傳送給消費者,可能會造成消費者記憶體爆滿;②當訊息處理比較慢的時,單一的消費者處理這些訊息可能很長時間,我們自然想到再新增一個消費者加快訊息的處理速度,但是這些訊息都被原來的消費者接收了,狀態為Unacked,所以這些訊息不會再傳送給新新增的消費者。針對這些問題怎麼去解決呢?
RabbitMQ提供的Qos(服務質量)可以完美解決上邊的問題,使用Qos時,Broker不會再把訊息一股腦的傳送給消費者,我們可以設定每次傳輸給消費者的訊息條數n,消費者把這n條訊息處理完成後,再獲取n條資料進行處理,這樣就不用擔心訊息丟失、服務端記憶體爆滿的問題了,因為沒有傳送的訊息狀態都是Ready,所以當我們新增一個消費者時,訊息也可以立即傳送給新增的消費者。注意Qos只有在消費端使用顯示確認時才有效,使用Qos的方式十分簡單,在消費端呼叫 channel.BasicQos() 方法即可,修改服務端程式碼如下:
static void Main(string[] args) { var factory = new ConnectionFactory() { //rabbitmq-server所在裝置ip,這裡就是本機 HostName = "127.0.0.1", UserName = "wyy",//使用者名稱 Password = "123321"//密碼 }; using (var connection = factory.CreateConnection()) { using (var channel = connection.CreateModel()) { channel.BasicQos(prefetchSize: 0, prefetchCount: 2, global: false); #region EventingBasicConsumer //定義消費者 var consumer = new EventingBasicConsumer(channel); //接收到訊息時執行的任務 consumer.Received += (model, ea) => { Thread.Sleep(1000 * 5); //處理完成,手動確認 channel.BasicAck(ea.DeliveryTag, false); Console.WriteLine($"處理訊息【{Encoding.UTF8.GetString(ea.Body)}】完成"); }; Console.WriteLine("消費者準備就緒...."); //處理訊息 channel.BasicConsume(queue: "myqueue", autoAck: false, consumer: consumer); Console.ReadKey(); #endregion } } }
清空一下queue中的訊息,重新啟動生產者,然後啟動消費者,開啟Web管理介面,看到狀態如下所示:
channel.BasicQos(prefetchSize: 0, prefetchCount: 2, global: false) 方法中引數prefetchSize為預取的長度,一般設定為0即可,表示長度不限;prefetchCount表示預取的條數,即傳送的最大訊息條數;global表示是否在Connection中全域性設定,true表示Connetion下的所有channel都設定為這個配置。
3 小結
本節演示了RabbitMQ的兩種消費者:EventingBasicConsumer和BasicGet。EventingBasicConsumer是基於長連線,釋出訂閱模式的消費方式,節省資源且實時性好,這是開發中最常用的消費模式。在一些需要消費者主動獲取訊息的場合,我們可以使用Get方式,Get方式是基於短連線的,請求響應模式的消費方式。
Qos可以設定消費者一次接收訊息的最大條數,能夠解決訊息擁堵時造成的消費者記憶體爆滿問題。Qos也比較適用於耗時任務佇列,當任務佇列中的任務很多時,使用Qos後我們可以隨時新增新的消費者來提高任務的處理效率。