一:摘要概述
- RabbitMQ(一) – 初識RabbitMQ :通過AMQP協議觸摸RabbitMQ整體結構設計
- RabbitMQ(二) – 交換器與隊列API探索 :完成RabbitMQ服務端結構基礎學習
- RabbitMQ(三) – 消息與隊列進階:本文將會是RabbitMQ基礎篇最倒數第二篇,同時也是進階RabbitMQ第一篇文章。內容將會由基礎消息生產衍生到TTL、Priority、MaxLength、DeadLetter等中階操作
二:基礎生產應用
2.1 基礎生產API
前面兩篇文章已經或多或少接觸到生產者與服務端交互,核心在於交換器,生產者不會與數據存儲的隊列直接耦合。即消息生產發送時必要結構應該有目標交換器、routingKey、消息體
參數 | 含義 |
---|---|
exchange | 交換器名稱 |
routingKey | 交換器路由消息的規則 |
BasicProperties | 消息的一些屬性特點,後續再深入跟進理解 |
body | 消息內容byte數組 |
// 前提已經創建好隊列與交換器並進行綁定
String exchangeName = "exchangeName";
String routingKey = "routingKey";
String message = "message";
// 基礎投遞消息
channel.basicPublish(exchangeName,routingKey,null,message.getBytes());
2.2 mandatory詳解
參數 | 含義 |
---|---|
mandatory | 至少匹配一個,若交換器中消息未匹配到任意一個隊列路由,可以通過ReturnListener監控獲取 |
immediate | 活躍匹配,不僅僅要求匹配到隊列,且要求該隊列有消費者連接。若無消費者連接則不會路由至該隊列,若最後沒有路由到任意一個隊列,可以通過ReturnListener獲取。3.0已經廢棄 ,設置爲true使用將發生異常信息 |
String exchangeName = "exchangeName";
String routingKey = "routingKey";
String message = "message";
// 測試mandatory
boolean mandatory = true, immediate = false;
String badRoutingKey = "badRoutingKey";
channel.basicPublish(exchangeName,badRoutingKey,mandatory,immediate,basicProperties,message.getBytes());
// 增加ReturnListener
ReturnListener returnListener = new ReturnListener() {
@Override
public void handleReturn(int replyCode, String replyText, String exchange,
String routingKey, AMQP.BasicProperties properties, byte[] body){
System.out.println(new String(body));
}
};
channel.addReturnListener(returnListener);
2.3 備胎交換器
通過添加ReturnListener確實可以保證交換器中未被路由的消息不丟失,但是引發的血案就是客戶端邏輯複雜化,反正個人不是很喜歡在正常邏輯中處理意外情況,這樣會導致後期迭代、維護成本上升。這時候就可以通過備胎交換器解決
交換器創建時綁定一個備胎交換器,當消息沒有對應路由隊列時就會轉發到這個備胎交換器,這與後面講得死信交換器隊列有一定相似之處。通過前面交換器創建代碼一直設置爲null的第五個參數Map實現,該參數通過key-value
形式設置一些屬性特徵,後面隊列也會使用到這個Map
key爲 alternate-exchange
,value爲備胎交換器名稱
// 創建備胎交換器與隊列
String alternateExchangeName = "alternateExchange",alternateQueueName = "alternateQueue";
String alternateBinding = "alternateBinding";
channel.exchangeDeclare(alternateExchangeName,BuiltinExchangeType.DIRECT,true,false,null);
channel.queueDeclare(alternateQueueName,true,false,false,null);
channel.queueBind(alternateQueueName,alternateExchangeName,alternateBinding);
// 將備胎交換器設置到參數中
Map<String,Object> exchangeArgumentMap = new HashMap<>();
exchangeArgumentMap.put("alternate-exchange",alternateExchangeName);
channel.exchangeDeclare("exchangeName", BuiltinExchangeType.DIRECT,true,false,exchangeArgumentMap);
- 通過控制面板查看交換器會有
AE
標識標識該交換器綁定有備胎交換器 - 備胎交換器路由到隊列使用的routingKey爲消息攜帶的routingKey
- 若原消息設置有消息過期等屬性轉發到備胎交換器路由後依然具備該屬性
三:Durable持久化
前面接觸到創建交換器Exchange、隊列Queue時都會有參數Durable持久化的存在,但是思考一下,隊列持久化以後消息就會持久化保存?答案是否定的,可以測試一下這個結論重啓RabbitMQ服務應用即可
若想要實現消息的持久化則還需要在發送消息時通過上一節講到的BasicProperties
屬性對象完成。該對象中存在諸多屬性表示消息的特性,不做集中講解,拆分到具體特性單元中講解。回到消息持久化操作上,BasicProperties
類中存在屬性deliveryMod
- 1:
表示消息不持久化
- 2:
表示消息持久化
如下所示代碼Demo,BasicProperties
是一個接口,實例化需要藉助與AMQP
接口的靜態內部類即AMQP.BasicProperties
實現。當然還有一點就是該內部類設計了建造者模式,大大方便客戶端API操作
AMQP.BasicProperties basicProperties = new AMQP.BasicProperties
.Builder()
.deliveryMode(2)
.build();
channel.basicPublish(exchangeName,routingKey,basicProperties,message.getBytes());
如果單純僅僅爲了消息持久化特性,可以使用MessageProperties
枚舉類,該類中封裝了幾種常用BasicProperties
對象實例,主要是針對持久化、優先級兩個屬性。最後提一點就是發送一條消息後可以重啓服務器進行驗證測試,測試過程結果本文不再贅述
// deliveryMod 爲 2表示持久化
// priority 爲 0 表示最低優先級
// contentType 爲 text/palin表示文本消息
AMQP.BasicProperties persistentTextPlain = MessageProperties.PERSISTENT_TEXT_PLAIN;
四:Priority優先級
生活中尊卑有序、長幼有別,技術上自然存在優先級概念,比如滴滴打車那種加錢插隊操作就可以使用消息優先級實現。上一節中提到了優先級屬性priority
,看過我前面Java隊列的文章其中就有優先級隊列ProrityBlockingQueue,當然這只是聯想而已,RabbitMQ中實現消息優先級還是依賴於對象BasicProperties
AMQP.BasicProperties basicProperties = new AMQP.BasicProperties
.Builder()
.priority(9)
.build();
channel.basicPublish(exchangeName,routingKey,basicProperties,message.getBytes());
數字越大優先級別越高,當然這個範圍一般在0-9之間即可。測試時發送不同優先級消息到隊列中消費查看結果即可,下圖所示發現只是按照消息插入的順序消費,優先級並未生效,你在搞什麼飛機?稍安勿躁,若實現消息優先級則必須設置隊列爲優先級隊列
設置優先級隊列操作在隊列實例化時通過參數map實現,前面一直展示代碼時queueDeclare()第五個參數都設置爲null,其實該參數爲Map集合,表示可以通過key-value的形式設置隊列的其它屬性。其中優先級key爲x-max-priority
,value表示優先級最大數值。通過控制檯可以看到隊列屬性多了Pri
標誌,表示該隊列爲優先級隊列
Map<String,Object> argumentMap = new HashMap<>();
argumentMap.put("x-max-priority",10);
channel.queueDeclare("queueName",true,false,false,argumentMap);
再使用代碼進行測試將會得到如下示例,消息生產是安裝優先級倒序,即0-9進行投遞。最後消費結果爲9-0,表示優先級生效,且證明優先級策略爲數值越大優先級越高
五:TTL 自動刪除過期
反正個人在學習這裏時腦海中總是聯想到Redis的TTL,實踐場景個人理解可以用到具備一定時效性消息上。如上層業務調用短信交付模塊功能,需要異步通過RabbitMQ通信交換消息,上層業務常見會Redis緩存生成校驗碼,且設置一定過期時效,若超過時限則用戶接收到短信也是無用的。這時就可以考慮將Redis中的TTL與RabbitMQ中的TTL時效一致,保證用戶接收到的驗證短信都具備可用性
自然,相對於Durable、Priority等特性需要隊列與消息合作完成而言,TTL自動刪除過期在這點上具有獨立性。可以做如下三個方面設置:
設置某個單獨消息的過期時間
設置某個隊列的過期時間
設置某個隊列中消息過期時間(這樣相當於統一設置隊列中所有消息過期TTL)
5.1 單消息TTL
某條消息的TTL時間藉助於BasicProperties
實例對象的expiration
屬性完成,單位爲ms。其餘的就不多贅述,測試時發送一條20s消息驗證即可
AMQP.BasicProperties basicProperties = new AMQP.BasicProperties
.Builder()
.expiration("20000")
.build();
channel.basicPublish(exchangeName,routingKey,basicProperties,message.getBytes());
5.2 隊列TTL
某個隊列長時間不進行操作後需要被刪除則可以使用設置隊列TTL時間實現,這個實現方式依舊採用Map方式,key爲x-expires
,value爲過期時限,單位ms
Map<String,Object> argumentMap = new HashMap<>();
argumentMap.put("x-expires",2000);
channel.queueDeclare("queueName",true,false,false,argumentMap);
最後通過面板可以看到隊列屬性中多了Exp
標識,該標識標識隊列爲自動過期刪除隊列
5.3 隊列消息TTL
某個隊列中所有消息過期時長一致,有必要在每個消息生產是設置單消息TTL?自然這時候就可以採用隊列消息TTL策略實現,其實現不言而喻自然是依賴於隊列實例化時設置
Map<String,Object> argumentMap = new HashMap<>();
argumentMap.put("x-message-ttl",2000);
channel.queueDeclare("queueName",true,false,false,argumentMap);
通過控制面板可以觀察到對列屬性多了TTL
標識,該標識表示隊列中消息到期自動刪除
六:MaxLength、MaxLengthBytes
消息可以無限積壓?可以傳輸任意大小消息?自然,RabbitMQ的設計考慮到這兩點,可以通過在隊列初始化的操作中設置。其實個人理解這兩個屬性作用意義不大,仁者見仁智者見智,知識完整性還是需要考慮的
key爲x-max-length表示隊列最大積壓消息數量限制
key爲x-max-length-bytes表示單消息最大字節數量
Map<String,Object> argumentMap = new HashMap<>();
argumentMap.put("x-max-length",1);
argumentMap.put("x-max-length-bytes",1024);
channel.queueDeclare("queueName",true,false,false,argumentMap);
Lim
標識標識隊列限制最大消息積壓數量,Lim B
表示隊列限制單消息最大字節數。兩個策略可以同時設置,共同生效。測試發現當超過最大積壓數量時會刪除頭部消息騰出空間存放新消息,這是RabbitMQ默認採用的策略drop-head
如果想實現拒絕新消息加入則可以使用參數x-overflow
實現,key爲x-overflow
,value爲reject-publish
表示拒絕接收新消息加入隊列。可以採用不同策略然後超過最大積壓數量設置值,查看最後消費到的數據是否與策略實現理論結果一致
argumentMap.put("x-overflow","reject-publish");
七:Dead-Letter 死信轉發
前面講到單消息自動過期TTL策略實現使用BasicProperties類屬性expiration即可,同時也說到了隊列積壓消息最大數量限制在隊列實例化時依賴x-max-length屬性實現,採用默認策略drop-head會刪除頭部消息。問題來了,這些操作都會導致消息丟失,這時候若想換個地方存儲這些消息怎麼辦?可採用隊列死信轉發實現,場景如下:
使用BasicProperties屬性expiration設置的TTL到期自動刪除消息
使用x-max-length限制消息最大積壓數量,且採用默認策略drop-head刪除的頭部消息
後面講消費者會講到的消息確認機制中拒絕確認消息,且未將消息放回隊列中刪除的消息
// 設置死信交換器
channel.exchangeDeclare("deadExchange",BuiltinExchangeType.DIRECT, true,false,null);
// 設置死信隊列
channel.queueDeclare("deadQueue",true,false,false,null);
// 綁定死信交換器與隊列
channel.queueBind("deadQueue","deadExchange","deadBinding");
// 設置正常隊列的死信交換器
Map<String,Object> argumentMap = new HashMap<>();
argumentMap.put("x-dead-letter-exchange","deadExchange");
argumentMap.put("x-dead-letter-routing-key","deadBinding");
channel.queueDeclare("queueName",true,false,false,argumentMap);
首先在控制面板上可以看到正常隊列有DLX
、DLK
兩個標識,表示該隊列設置了死信交換器和死信路由routingKey。其次最終結果是TTL到期自動刪除的消息轉發到了deadQueue
中。最後需要說明若不設置x-dead-letter-routing-key
參數,死信交換器將採用消息自身攜帶的routingKey
進行路由
思考題:
如何用死信隊列實現延遲隊列,並考慮具體場景如訂單倒計時關閉