一:摘要概述
經過前面三篇文章的學習,對於RabbitMQ中間件應該處於撥開雲霧見青天階段。本文將趁熱打鐵,完善RabbitMQ基礎應用最後一個消費版塊。當然文中會持續深入講解有關消息分發、消費端確認等中階特性
二:消息消費
MQ隊列可以理解爲物品寄存中心,放進去總要拿出來用,一直放着沒有利息還會持久增加成本引發系列問題。MQ存儲的消息使用有兩種途徑,RabbitMQ服務推送、消費者客戶端拉取
2.1 拉取消息
通過baiscGet()拉取RabbitMQ服務端消息有以下幾點需要注意:
一次只能消費一條消息,千萬別使用用循環代替後面的baiscConsume()
- 前面講的隊列創建參數有AutoDelete,但是注意這個
自動刪除前提爲至少有一個消費者連接到隊列
,並且當所有消費者斷開時刪除,這裏通過basicGet()消費不包含在內
// 設置隊列自動刪除
channel.queueDeclare("autoDeleteQueue", true, false, true, null);
channel.basicPublish("", "autoDeleteQueue", null, "測試".getBytes());
// 驗證basicGet不觸發隊列自動刪除
channel.basicGet("autoDeleteQueue", true);
當然basicGet()
方法自身API比較簡單,第一個參數指明消費隊列,第二個參數設置是否自動應答即AutoAck。返回對象也就封裝Envelope
、BasicProperties
、body
消息體等,具體信息如下表所示:
序號 | 方法參數 | 含義 |
---|---|---|
1 | queue | 隊列名稱,指定消費者消費隊列 |
2 | autoAck | 自動應答,打開爲true後RabbitMQ應用送出消息將立即刪除 |
序號 | 返回值 | 備註 |
---|---|---|
1 | envelope | 包含deliveryTag、exchange、routingKey等信息 |
2 | props | BasicProperties對象,即消息生產時設置的該對象特性 |
3 | body | 消息體byte數組 |
4 | messageCount | 消息數量 |
2.2 推送消息
相對於拉取消息而言,basicConsume()
推送消息更加符合生產環境的需求,持續監控消費隊列。自然其API也更加複雜,常用系列重載參數如下表所示:
序號 | 方法參數 | 含義 |
---|---|---|
1 | queue | 消費隊列名稱 |
2 | autoAck | 自動確認提交 |
3 | consumerTag | 消費者唯一標識 |
4 | noLocal | 不消費同一Connection連接生產的消息 |
5 | consumer | 具體組織消費邏輯對象,裏面提供系列重載方法用戶消費邏輯組裝 |
推送消息消費最後一般都是採用實現Consumer
接口亦或是繼承DefaultConsumer
類,DefaultConsumer實現了接口Consumer,但是大多數方法都是空實現,需要重寫其中的邏輯。其中重要方法執行時間如下表所示:
DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
@Override
public void handleDelivery (String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// 處理消息邏輯
}
};
channel.basicConsume("queueName",true,"consumerTag",defaultConsumer);
序號 | 方法 | 執行時間 |
---|---|---|
1 | handleDelivery() | 消費消息邏輯 |
2 | handleConsumeOk() | 第一篇文章就就講到Consume-Ok命令在delivery命令前,即每個消費者開始消費前都會執行該方法一次 |
3 | handleShutdownSignal() | 當連接Connection / 信道Channel斷開關閉時執行一次 |
4 | handleRecoverOk() | baiscRecover()命令隊列重發未確認消息,當未確認消息被重發前執行這個方法 |
5 | handleCancelOk() | baiscCancel()顯示取消消費者,當消費者被取消時指定這個方法 |
2.3 隊列重發
上面講到Consumer的方法handleRecoverOk()
將會在消息重發時調用,顯示的調用消息重發方法爲basicRecover()
。該方法只有一個參數:
true
:表示可以將重發消息發送給其它消費者false
:表示只能將消息發送給相同的消費者
三:消息確認
第一篇文章中有一個命令是Basic.Ack用於客戶端向服務端反饋確認消息已經正常消費,當接收到命令後RabbityMQ服務端纔會刪除消息,從消費者客戶端確保消息不會丟失。自然有確認就有拒絕確認,本節將介紹basicAck()、basicReject()、basicNack()
3.1 確認消費basicAck
RabbitMQ反饋確認消費通過命令basicAck()
實現,該方法具備兩個參數deliveryTag
和multiple
channel.basicAck(envelope.getDeliveryTag(),false);
- deliveryTag:確認消息的編號,這是每個消息被消費時都會分配一個遞增唯一編號
- multiple:批量確認,true表示所有編號小於目前確認消息編號的待確認消息都會被確認,false則只確認當前消息
特別注意:消息的編碼是每個信道Channel範圍的,批量確認操作也是針對當前Channel信道的操作。請務必記住這個範圍
3.2 拒絕確認basicReject
程序在消費消息過程中拋出異常,或者是消息需要重複消費,這時候就可以將消費的消息拒絕確認。拒絕確認的消息有兩種去處,刪除、放回隊列,通過參數requeue
控制,拒絕確認的消息放回隊列時會放置在隊列首位,拒絕消息不放回隊列可以搭配死信隊列使用
void basicReject(long deliveryTag, boolean requeue) throws IOException;
3.3 拒絕確認basicNack
確認消費可以批量確認,爲什麼拒絕確認消息不能批量拒絕?所以爲了補充basicReject()不足提出了basicNack()
。這個API相對於basicReject()而言多了一個參數multiple
,效果與批量確認一致
void basicNack(long deliveryTag, boolean multiple, boolean requeue)
throws IOException;
四:消息預取
消息消費RabbitMQ採取的策略就是輪詢機制,將每個消息發送給唯一的消費者。每個消費者獲取到的消息都是平均的,這樣的機制會導致下列問題:
- 某些消息耗時超長,平均分配消息後可能導致某些消費者積壓過多未消費消息,而同時某些消費者處於空閒狀態,導致系統吞吐量下降
通過下面代碼可以告訴RabbitMQ服務端,我只接受prefetchCount
數量的未確認消息,當消費者客戶端未確認消息達到限定值後服務端將不會給該消費者推送數據。第二個參數的含義如下表:
參數值 | 含義 |
---|---|
false | 默認值,單獨應用於信道上所有消費者 |
true | 信道上所有消費者共享 |
void basicQos(int prefetchCount, boolean global) throws IOException;
合理的消息預取配合消費端手動ACK確認機制可以很好的優化平衡消費者性能,這個預取數量問題可以根據隊列消息增長率與消費端消息處理效率平衡
五:RPC
使用RabbitMQ完成RPC操作其實比較簡單,就是使用了前面講到過的BasicPeoperties
對象。發送消息時消息可以攜帶該對象,前面使用到了對象的deliveryMod
持久化、priority
優先級、expiration
自動過期刪除屬性。這裏RPC將會使用到replyTo
屬性告訴RPC服務端執行完畢後回調隊列地址,correlationId
用於標識請求唯一ID
5.1 RPC客戶端
- UUID生成correlationId請求唯一標識ID,客戶端消費回調隊列時確認歸屬與自己的請求回調
- ArrayBlockingQueue阻塞隊列用於阻塞主線程等待RPC服務端完成邏輯以後的回調
- 如果想限制RPC遠程超時時間則可以在阻塞隊列等待方法take()中添加最大等待時長
@SneakyThrows
public static void main (String[] args) {
Channel channel = TemplateConfigServiceImpl.createChannel();
// 創建BasicProperties賦值replyTo回調隊列名稱、correlationId請求唯一標識ID
String correlationId = UUID.randomUUID().toString();
String replyQueue = "queue1";
AMQP.BasicProperties basicProperties = new AMQP.BasicProperties.Builder().replyTo(replyQueue).correlationId(correlationId).build();
// 客戶端向服務端監控隊列發送消息
String rpcQueue = "queueName";
channel.basicPublish("",rpcQueue,basicProperties,"RPC測試".getBytes());
// 創建阻塞隊列等到消息回調
ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(1);
// 監控回調隊列消息獲取遠程調用結果
DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
@Override
public void handleDelivery (String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// 校驗消息唯一標識是否匹配
String replyCorrelationId = properties.getCorrelationId();
long deliveryTag = envelope.getDeliveryTag();
if (correlationId.equals(replyCorrelationId)){
// 將回調消息放到阻塞隊列中
arrayBlockingQueue.offer(new String(body));
channel.basicAck(deliveryTag,false);
}else {
// 不匹配消息放回隊列
channel.basicReject(deliveryTag,true);
}
}
};
channel.basicConsume(replyQueue,false,"ConsumerTag",defaultConsumer);
// 阻塞等待阻塞隊列中消息處理後續邏輯
String take = arrayBlockingQueue.take();
System.out.println(take);
}
5.2 RPC服務端
整體RPC服務端、客戶端實現都是最簡陋的自行車設計,如果想要更復雜的邏輯請自行完成
@SneakyThrows
public static void main (String[] args) {
Channel channel = TemplateConfigServiceImpl.createChannel();
// 監控RPC隊列消息執行任務
String rpcQueue = "queueName";
DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
@Override
public void handleDelivery (String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// 執行計算邏輯
System.out.println("RPC遠程服務端開始執行任務");
System.out.println(new String(body));
// 組裝回調消息
String replyTo = properties.getReplyTo();
channel.basicPublish("",replyTo,properties,"RPC遠程計算任務完成".getBytes());
// 確認消息消費
long deliveryTag = envelope.getDeliveryTag();
channel.basicAck(deliveryTag,false);
}
};
channel.basicConsume(rpcQueue,false,"ConsumerTag",defaultConsumer);
}