說起RabbitMQ大家第一時間應該想到的就是異步隊列,關於異步隊列的話題簡直太多了,各位同學在園子裏一搜便知。我第一次聽異步隊列這個名詞感覺非常高大上😀,想到這項技術必須要學。但是學習的任何一門技術沒經過項目的洗禮,都似乎少了點什麼。嗯。是的。只有在企業級開發中,才能找到自己的遺漏的知識點。所以大家一定要想辦法將自己學習到的東西糅合進項目中。以我現在一貫的作風就是學習、鞏固、總結與分享。雖然介紹RabbitMQ這類博客已經很多了,但是人家寫的畢竟是人家寫的,我要把自己的學習心得記錄下來,一來鞏固自己的知識,二來可以幫到其它的小夥伴。😊
首先說一下隊列.隊列的本質是一種先進先出的線性存儲結構。注意不要和棧弄反了,棧的存儲結構是先進後出
那怎樣理解異步隊列?
舉個例子:假如在電商系統中,用戶下單成功之後。A服務要拿到下單信息發送郵件(350ms)給用戶,B服務要拿到下單信息發送短信(400ms)給用戶,C服務要拿到下單信息記錄日誌(300ms),如果三個服務利用RPC遠程調用下單服務提供的api,在排隊等候時(可能有分佈式鎖)將要耗費1050ms,而且耦合太高了,如果下單服務改由另一個服務承擔那麼ABC服務都得跟着改,接下來就變成下面這樣。
用戶下單成功後直接將數據存進消息隊列(數據庫也會存一份),ABC三服務直接從消息隊列中拿下單信息,不再排隊等候(異步),時間耗費400ms。性能提升了一倍之多。而且誰將數據放入消息隊列ABC三服務根本不用管,它們現在只認數據不認人.😄
再來說一下RabbitMQ.RabbitMQ是採用Erlang語言實現的消息中間件。RabbitMQ幾乎支持所有的常用語言,並且提供了用戶管理界面,可以方便用戶管理和監控消息。在使用RabbitMQ之前我們先安裝她。這裏的話我只說一下在Linux下使用小鯨魚安裝
docker pull rabbitmq:management
docker run -d -p 15672:15672 -p 5672:5672 --name rabbitmq rabbitmq:management
不出意外的話容器已經啓動了,如果有問題的話就看日誌.
默認情況下訪問RabbitMQ 用戶名和密碼都是 "guest" ,但是這個這個賬戶有限制,默認只能通過本地網絡訪問,遠程連接的話受限.
這裏的話要執行以下命令. ps:命令是網上找的😉
docker exec -it rabbitmq bash --進入容器
rabbitmq-plugins enable rabbitmq_management --開啓插件命令
這裏再說一下生產者和消費者。生產者就是往消息隊列中加入(創建)數據的一方。消費者就是拿到(接收)消息的一方。
接下來在代碼中簡單演示一下.
新建兩個控制檯程序。引入包RabbitMQ.Client.
生產者代碼:
static void Main(string[] args) { //創建連接工廠 var factory = new ConnectionFactory { UserName = "guest",//用戶名 Password = "guest",//密碼 HostName = "***.**.***.***",//rabbitmq ip Port = 5672,//端口號 }; //創建連接 var connection = factory.CreateConnection(); //創建信道 var channel = connection.CreateModel(); //創建一個隊列,名稱爲:RabbitMQStudy channel.QueueDeclare("RabbitMQStudy", false, false, false, null); Console.WriteLine("已連接到RabbitMQ.^-^,可向隊列投遞消息!"); Console.WriteLine("輸入close則關閉連接"); string text = string.Empty; bool flag = true; while (flag) { Console.WriteLine("請輸入數據......"); text = Console.ReadLine(); switch (text) { case "close": flag = false; continue; default: flag = true; break; } var bytes = Encoding.UTF8.GetBytes(text); //發佈消息 channel.BasicPublish("", "RabbitMQStudy", null, bytes); Console.WriteLine("消息已發送成功!"); Console.WriteLine(new string('-', 100)); } Console.WriteLine("連接已關閉......"); //關閉資源 channel.Close(); //釋放連接 connection.Close(); }
消費者代碼:
static void Main(string[] args) { //創建連接工廠 ConnectionFactory factory = new ConnectionFactory { UserName = "guest",//用戶名 Password = "guest",//密碼 HostName = "***.**.***.***",//rabbitmq ip Port = 5672,//端口號 }; //創建連接 var connection = factory.CreateConnection(); //創建信道 var channel = connection.CreateModel(); //var consumer = new DefaultBasicConsumer(channel); var consumer = new EventingBasicConsumer(channel); //爲消費者送達消息時產生的事件 consumer.Received += (ch, ea) => { var message = ea.Body.ToArray();//接收到的消息 Console.WriteLine($"接收到信息:{Encoding.UTF8.GetString(message)}"); //消息已被消費 channel.BasicAck(ea.DeliveryTag, false); Console.WriteLine("消息已成功接收並消費!"); Console.WriteLine(new string('-', 100)); }; //啓動消費者並設置爲手動應答 String consumerTag = channel.BasicConsume("RabbitMQStudy", false, consumer); Console.WriteLine("消費者已啓動成功!"); Console.ReadKey(); channel.Dispose(); connection.Close(); }
開啓了三個消費者,這時隊列中的消息會被平均分攤(訂閱了同一個隊列).RabbitMQ有輪詢的機制,所以不是每個消費者都可以收到所有的消息井處理。
下面說一下三個重要的知識點:交換器、路由鍵、綁定
交換器:生產者向消息隊列發送消息數據時,其實消息先到交換器再由交換器路由到相應的隊列。這裏有同學就會問了,我們剛剛上面的代碼並沒有申明交換器啊,其實這裏我們發送消息時exchange參數爲(" "),相當於默認創建好的一個交換器(類型爲direct)。直接用RouteKey指定隊列名即可.
交換器一共有四種類型:1.fanout:此類型的交換器會將發送到該交換器的的消息路由到所有與該交換器綁定的隊列中 2.directc:如果交換器類型爲direct類型,那麼RoutingKey(路由鍵,相當於消息投送至與交換器綁定的哪個隊列)和BindingKey(綁定鍵,交換器與隊列相綁定有一個BindingKey)需要完全匹配(相同)。 3.topic:topic是升級版的direct類型的交換器,與direct類型交換器相同的是topic類型的交換器也是將消息路由到RoutingKey與BindingKey相匹配的隊列中。與direct類型交換器不同的是topic類型的交換器有模糊匹配。 例如:我們發送消息時會路由到指定RouteKey隊列中.交換器在與隊列綁定的時候,BindingKey可以是模糊類型的.RouteKey和BindingKey都是以點號“.”分割的字符串.BindingKey的字符串可以存在“ * ”和“ # ”特殊字符。在以點號“ . ”爲分割的字符串中,“ . ”用於匹配一個單詞," # "用於匹配多個(或零個)單詞. 4.headers:這個交換器我在這裏不做介紹,我沒用過🙂,上面介紹的三個是最常用的了。
如下圖所示申明瞭一個topic類型的交換器,當路由鍵爲core.never.net的消息會同時路由到隊列1和隊列2中。當路由鍵爲go.never.cn的消息也會同時路由到隊列1與隊列2中。當路由鍵爲www.bilibili.com的消息會路由到隊列2中。
舉個例子,在生產者代碼中再加幾句代碼:
//和上面一樣,代碼省略...
//創建一個隊列,名稱爲:RabbitMQStudy channel.QueueDeclare("RabbitMQStudy", false, false, false, null); //創建一個隊列,名稱爲:RabbitMQStudy1 channel.QueueDeclare("RabbitMQStudy1", false, false, false, null); //聲明一個交換器 channel.ExchangeDeclare("exange", "topic", true, false, null); //綁定 channel.QueueBind("RabbitMQStudy", "exange","#.routeKey"); //綁定 channel.QueueBind("RabbitMQStudy1", "exange", "*.com");
//和上面一樣,代碼省略...
//發佈消息
channel.BasicPublish("exange", "core.routeKey", null, bytes);
//和上面一樣,代碼省略...
在建一個控制檯程序(消費者),和第一個消費者的代碼一樣,只不過兩個消費者監聽設置的隊列名不一樣。一個是RabbitMQStudy,一個是RabbitMQStudy1。
//啓動消費者並設置爲手動應答 String consumerTag = channel.BasicConsume("RabbitMQStudy1", false, consumer);
運行... ...
可以看到只有消費者1接收到了消息,模糊匹配的作用就展現出來了.
路由鍵:生產者將消息發送給交換器時,會指定一個路由key,用於設定這個消息的路由規則,路由key需要與交換器類型和綁定key聯合使用纔能有效。
綁定:RabbitMQ中通過綁定將交換器與隊列進行關聯,在交換器與隊列進行綁定時會有一個綁定鍵,這樣結合路由鍵RabbitMQ就會知道將消息投遞到哪個隊列中。
爭對上述代碼及文字我們來總結一下 消息隊列的優缺點:1.解耦,我上面提到了,消費者只需要拿數據,數據的投遞方是誰它一點都不過問。 2.削峯.當我們設計一個秒殺業務的時候,假如成千上萬的請求湧入,服務器的壓力過大。爲了緩解壓力我們可以將請求先放入隊列中,服務器一定時間內可以承受多少請求就拿多少。 3.異步.我上面也提到了.直接從消息隊列中拿數據,無需排隊等候,大大提高了系統性能。 RabbitMQ的調用過程:生產者:1.先創建連接工廠,與RabbitMQ建立連接。 2.創建信道。 3.聲明一個交換器,設置交換器類型、是否持久化等。 4.聲明隊列,設置隊列是否排他、是否持久化等。 5.將交換器與隊列綁定,設置RouteKey。 6.發送消息至交換器,並設置交換器名稱、路由鍵、消息主體等。 7.交換器根據路由鍵和綁定的隊列相匹配,投送消息至隊列中。 8.如果沒有找到相應隊列,會根據生產者配置的屬性丟棄消息或者退回給生產者。 9.關閉連接。 消費者:1.先創建連接工廠,與RabbitMQ建立連接。 2.創建信道。 3.等待生產者投遞消息至隊列,消費者接收消息。 4.消費者確認接收到消息 5.RabbitMQ從隊列中刪除已確認的消息。 6.關閉連接。
今天就這麼多,後續文章會一一介紹RabbitMQ其它特性。
如有不足,請見諒!