之前文章說過,如果使用 RabbitMQ,盡可能使用架構,而不要去使用 RabbitMQ 提供的 Java 版用戶端。
細說起來,其實還是因為 RabbitMQ 用戶端的使用有很多的注意事項,稍微不注意,就容易翻車。
我是 2013 年就開始用起了 RabbitMQ,一路使用,一路和它一起成長。當時,由于用的早,市面上也沒有特别成熟的 RabbitMQ 用戶端架構。是以,不得已之下,隻好自己做了一套用戶端。
在這其中,正好也有了許多獨特的經驗也和大家分享一下,以免後來者陷入“後人哀之而不鑒之,亦使後人而複哀後人也”的套娃中。
一、那麼,就先從網絡連接配接開始吧
1. 應該長久生存的連接配接
在 RabbitMQ 中,由于需要用戶端和伺服器端進行握手,是以導緻用戶端和伺服器端的連接配接如果要成功建立,需要很高的成本。
每一個連接配接的建立至少需要 7 個 TCP 包,這還隻是普通連接配接。如果需要 TLS 的參與,則 TCP 包會更多。
而且,RabbitMQ 中主要是以 Channel 方式通信,是以,每次建立完 Connection 網絡連接配接,還得建立 Channel,這又需要 2 個 TCP 包。
如果,每次用完,再把連接配接關閉,首先還要關閉已經建立的 Channel,這也需要 2 個 TCP 包。
然後,再關閉已經建立好的 Connection 連接配接,又需要 2 個 TCP 包。
咱們算算,如果一個連接配接從建立到關閉,一共需要多少個 TCP 包?
7 + 2 + 2 + 2 = 13
一共需要 13 個包。這個成本是很昂貴的。
是以,在 RabbitMQ 中,連接配接最好緩存起來,重複使用更好。
2. Channel 還是獨占好
在 RabbitMQ 自己的用戶端中,Channel 出于性能原因,并不是線程安全的。
而如果咱們為了線程共用,給 Channel 人為的在外部加上鎖,本身就和 RabbitMQ 的 Channel 設計意圖是沖突的。
是以,最好的辦法就是一個線程一個 Channel。
3. Channel 最好也别關
就像連接配接應該緩存起來那樣,Channel 的打開和關閉也需要時間成本,而且沒有必要去重新建立 Channel,是以,Channel 也應該緩存起來重用。
4. 别把消費和發送的連接配接搞在一起
把消費和發送的連接配接搞在一起,這是個很容易犯的錯誤!
我們用 RabbitMQ 的時候,我們自己的系統本身大部分都是既要發消息也要收消息的。對于這種情況,有很多程式員走了極端:
他們覺得 RabbitMQ 連接配接成本高,是以省着用。于是就把發消息和收消息的連接配接混在一起,使用同一個 TCP 連接配接。
這很可能會埋一個大雷。
因為,當我們發消息很頻繁的時候,我們收消息也是走的同一個 TCP 通道,收完了消息,用戶端還要給 RabbitMQ 伺服器端一個 ACK。
RabbitMQ 伺服器端,對于每個 TCP 連接配接都會配置設定專門的程序,如果遇到這個程序繁忙,這個 ACK 很可能被丢棄,又或者等待處理的時間過長。而這種情況又會導緻 RabbitMQ 中的未确認消息會被堆積的越來越多,影響到整套系統。
是以,消費和發送的連接配接必須分開,各幹各的事情。
5. 别搞太多連接配接和 Channel,RabbitMQ 的 Web 受不了
RabbitMQ 的 Web 插件會收集很多連接配接,和其對應 Channel 的相關資料。
如果連接配接和 Channel 堆積太多了,整個 Web 打開會非常慢,幾乎無法對 RabbitMQ 進行管理。是以,要注意限制連接配接和 Channel 的數量。
二、消息很寶貴,千萬别亂抛棄哦
用來通信的消息是很寶貴的。
因為每條消息都可能攜帶了關鍵的資料和資訊。是以,保證消息不丢失,需要根據消息的重要性,采取很多的措施。
1. 小心,Queue 存在再發消息
一條消息,在 RabbitMQ 中會先發到 Exchange,再由 Exchange 交給對應的 Queue。
而當 Queue 不存在,或者沒比對到合适的 Queue 的時候,預設就會把消息發到系統中的 /dev/null 中。
而且還不會報錯。
這個坑當年把我坑慘了!我猜這個坑無數人踩過吧。
是以,在發送消息的時候,最好通過 declare passive 這種方法去探測下隊列是否存在,保證消息發送不會丢的莫名其妙。
2. 收到消息請告訴我
在使用 RabbitMQ 用戶端的時候,發送消息,一定要考慮使用 confirm 機制。
這個機制就是當消息收到了,RabbitMQ 會往用戶端發送一個通知,用戶端收到這個通知後,如果存在一個 confirm 處理器,那麼就會回調這個處理器處理。這時候,我們就能確定消息是被中間件收到了。
是以,一定要考慮使用 confirm 處理器去確定消息被 RabbitMQ 伺服器收到。
3. 有時候消息出了問題我也需要知道
在某些業務裡,可能需要知道消息發送失敗的場景,以便執行失敗的處理邏輯。這時候,就要考慮 RabbitMQ 用戶端的 return 機制。
這個機制就是當消息在伺服器端路由的時候出現了錯誤,比如沒有 Exchange、或者 RoutingKey 不存在,則 RabbitMQ 會傳回一個響應給用戶端。用戶端收到後會回調 return 的處理器。這時候,用戶端所在系統就能感覺到這種錯誤了,進而進行對應的處理。
4. 為了一定不丢消息我也是拼了
還有的時候,消息需要處理強一緻性這種事務性質的業務。這時候,就必須開啟 RabbitMQ 的事務模式。但是,這個模式會導緻整體 RabbitMQ 的性能下降 250 倍。
一般沒有必要,不建議開啟。
5. 把消息寫到磁盤上
一般來說,為了防止消息丢失,需要在 RabbitMQ 伺服器收到消息的時候,先持久化消息到磁盤上,防止伺服器狀态出現問題,消息丢失。
但是,持久化消息,必須先持久化隊列,持久化隊列完還不行,還必須把消息的 delivery mode 設定為 2,這樣才能把消息存到磁盤。但是,這種行為會讓整個 RabbitMQ 的性能下降 60%。
這種可以根據實際情況進行抉擇。
三、對于收消息這件事,别由着性子來
1. 能一次拿多個幹嘛要一次隻拿一個
很多時候,一些 RabbitMQ 的新手,覺得如果在一個 mainloop 類似的無限循環裡,去主動擷取消息,會更加及時的擷取到消息,也會擁有更加出色的性能。是以,他們會使用 get 這種行為去取代 consume 這種行為。
這時候,他們其實已經踩進了大坑。
為了能主動 get 伺服器消息,很多新手會去寫一個無限循環,然後不斷嘗試去 RabbitMQ 伺服器端擷取消息。但是,get 方法,其實是隻去擷取了隊列中的第一條消息。
而采用 consume 方式呢,它的預設方式是隻要有消息,就會批量的拿,直到拿光所有還沒消費過的消息。
一個是一條條拿,一個是批量拿,哪個效率更高一目了然。
是以,盡量采用 consume 方式擷取消息。
2. 拿消息也要講方法論的
消費消息的時候,其實最難掌握的就是:
一次我們到底要取多少條消息?
對于 RabbitMQ 來講,如果我們不對消費行為做限制,他會有多少消息就擷取多少消息。這就造成了一個問題:
如果消息過多,我們一次性把消息讀取到記憶體,很可能就會把應用的記憶體擠崩掉。
是以,我們要對這種情況做一些限制。
這時候,需要限制一次擷取消息的數量,一般來講,當我們的業務是異步發送,異步消費,不需要實時給回響應的時候,經驗資料是一次擷取 1000 條。
當然,系統和系統不一樣,硬體條件也不一樣,大家可以根據實際的情況來設定一次性擷取的消息數量。
重點要說說同步。
在很多時候,我們需要通過 RabbitMQ 傳送消息,并能通過臨時隊列等技巧去實時傳回處理結果。這時候,就沒辦法一次抓多條資料進行處理了,因為,有發送端在等處理結果,依次處理,再依次傳回,黃花菜都涼了。
而且大部分時候,這種同步等待響應的業務是有順序要求的。是以,也不能并行同時抓出多條資訊處理。那麼,彼時,設定每次隻消費一條消息就是理所應當的了。
最後
從上面的内容中,你也看到了,RabbitMQ 用戶端如果要使用,對新手是多可惡的一件事情,各種坑,各種複雜性。
是以,如果你覺得 Spring 之類的 AMQP 用戶端架構合你心意,那麼你就使用它。
但是,Spring 的東西有個毛病,如果你要用它,你的應用必須也都要用 Spring。有些時候,也沒有這種必要。這時候,你就可以根據我說的這些注意事項和經驗,自己開發一套 RabbitMQ 的封裝架構,去降低 RabbitMQ 的使用門檻。
你好,我是四猿外。
一家上市公司的技術總監,管理的技術團隊一百餘人。
我從一名非計算機專業的畢業生,轉行到程式員,一路打拼,一路成長。
我會把自己的成長故事寫成文章,把枯燥的技術文章寫成故事。
歡迎關注我的公衆号,關注之後還可以擷取算法、高并發等幹貨學習資料。
我建了一個讀者交流群,裡面大部分是程式員,一起聊技術、工作、八卦。歡迎加我微信,拉你入群。