開心一刻
上午一好哥們微信我哥們:哥們在幹嘛,晚上出來吃飯我:就我倆嗎哥們:對啊我:那多沒意思,我叫倆女的出來哥們:好啊,哈哈哈晚上吃完飯到家後,我給哥們發訊息我:今天吃的真開心,下次繼續哥們:開心尼瑪呢!下次再叫你老婆和你女兒來,我特麼踢死你
寫在前面
正文開始之前了,我們先來正確審視下文章標題:不依賴 Spring,你會如何自實現 RabbitMQ 訊息的消費
,主要從兩點來進行審視
- 不依賴 Spring
作為一個 Javaer
,關於 Spring
的重要性,我相信你們都非常清楚;回頭看看你們開發的專案,是不是都是基於 Spring 的?如果不依賴 Spring,你們還能繼續開發嗎?不過話說回來,既然 Spring 能帶來諸多便利,該用還得用,不要頭鐵,不要造低效輪子!
如果能造出比 Spring 優秀的輪子,那你應該造!
你們可能會說:不依賴 Spring 就不依賴嘛,我可以依賴 Spring Boot
噻;你們要是這麼聊天,那就沒法聊了
Spring Boot 是不是基於 Spring 的?沒有 Spring,Spring Boot 也是跑不起來的;不依賴 Spring
的言外之意就是不依賴 Spring 生態,當然也包括 Spring Boot
關於 不依賴 Spring
,我就當你們審視清楚了哦
2. 依賴 RabbitMQ Java Client
與 RabbitMQ 服務端的互動,咱們就不要逞強去自實現了,老實點用官方提供的 Java Client 就好
<dependency>
<groupId>com.rabbitmqgroupId>
<artifactId>amqp-clientartifactId>
<version>5.7.3version>
dependency>
注意 client 版本要與 RabbitMQ 版本相容
所以文章標題就可以轉換成
只依賴 RabbitMQ Java Client,不依賴 Spring,如何自實現 RabbitMQ 訊息的消費
另外,我再帶你們回顧下 RabbitMQ 的 Connection 和 Channel
- Connection
Connection
是客戶端與 RabbitMQ 伺服器之間的一個 TCP 連線,它是進行通訊的基礎,允許客戶端傳送命令到 RabbitMQ 伺服器並接收響應;Connection 是比較重的資源,不能隨意建立與關閉,一般會以 池
的方式進行管理。每個 Connection 可以包含多個 Channel
2. Channel
Channel
是 多路複用
連線(Connection)中的一條獨立的雙向資料流通道。客戶端與 RabbitMQ 服務端之間的大多數操作都是在 Channel 上進行的,而不是在 Connection 上直接進行。Channel 比 Connection 更輕量級,可以在同一連線中建立多個 Channel 以實現併發處理
Channel 與 Consumer 之間的關係是一對多的,具體來說,一個 Channel 可以繫結多個 Consumer,但每個 Consumer 只能繫結到一個
自實現
我們採取主幹到枝葉的實現方式,逐步實現並完善 RabbitMQ 訊息的消費
主流程
依賴 RabbitMQ Java Client 來消費 RabbitMQ 的訊息,程式碼實現非常簡單,網上一搜一大把
/**
* @author: 青石路
*/
public class RabbitTest1 {
private static final String QUEUE_NAME = "qsl.queue";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = initConnectionFactory();
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.basicConsume(QUEUE_NAME, false, new QslConsumer(channel));
System.out.println(Thread.currentThread().getName() + " 執行緒執行完畢,消費者:" + consumerTag + "已經就緒");
}
public static ConnectionFactory initConnectionFactory() {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("10.5.108.226");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("admin");
factory.setVirtualHost("/");
factory.setConnectionTimeout(30000);
return factory;
}
static class QslConsumer extends DefaultConsumer {
QslConsumer(Channel channel) {
super(channel);
}
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, StandardCharsets.UTF_8);
System.out.println(Thread.currentThread().getName()+ " 收到訊息:" + message);
this.getChannel().basicAck(envelope.getDeliveryTag(), false);
}
}
}
是不是很簡單?
這裡我得補充下,
exchange
、queue
沒有在程式碼中宣告,繫結關係也沒有宣告,是為了簡化程式碼,因為文章標題是消費
;實際exchange
、queue
、binding
這些已經存在,如下圖所示
上述程式碼,我相信你們都能看懂,主要強調下 2 點
- 訊息是否自動 Ack
對應程式碼
channel.basicConsume(QUEUE_NAME, false, new QslConsumer(channel));
basicConsume
的第二個引數,其註釋如下
autoAck
為 true
表示訊息在送達到 Consumer 後被 RabbitMQ 服務端確認,訊息就會從佇列中剔除了;autoAck
為 false
表示 Consumer 需要顯式的向 RabbitMQ 服務端進行訊息確認
因為我們將 autoAck
設定成了 true
,所以 main 執行緒存活的時間內,5 個訊息被送達到 main 執行緒後就被 RabbitMQ 服務端確認了,也就從佇列中刪除了
2. 手動確認
如果 Consumer 的 autoAck
設定的是 false
,那麼需要顯示的進行訊息確認
this.getChannel().basicAck(envelope.getDeliveryTag(), false);
否則 RabbitMQ 服務端會將訊息一直保留在佇列中,反覆投遞
執行 main 方法,控制檯輸出如下
我們去 RabbitMQ 控制檯看下佇列 qsl.queue
的消費者
Consumer tag
值是:amq.ctag-PxjqYiujeCvyYlgtvMz9EQ
,與控制檯的輸出一致;我們手動往佇列中傳送一條訊息
控制檯輸出如下
自此,主流程就通了,此時已經實現 RabbitMQ 訊息的消費
多消費者
單消費者肯定存在效能瓶頸,所以我們需要支援多消費者,並且是同個佇列的多消費者;實現方式也很簡單,只需要調整下 main 方法即可
public static void main(String[] args) throws Exception {
ConnectionFactory factory = initConnectionFactory();
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
QslConsumer qslConsumer = new QslConsumer(channel);
String consumerTag1 = channel.basicConsume(QUEUE_NAME, false, qslConsumer);
String consumerTag2 = channel.basicConsume(QUEUE_NAME, false, qslConsumer);
String consumerTag3 = channel.basicConsume(QUEUE_NAME, false, qslConsumer);
System.out.println(Thread.currentThread().getName() + " 執行緒執行完畢,消費者["
+ Arrays.asList(consumerTag1, consumerTag2, consumerTag3) + "]已經就緒");
}
執行main 方法後 Channel 與 Consumer 關係如下
此時是同個 Channel 繫結了 3 個不同的 Consumer;當然也可以一對一繫結,main 方法調整如下
public static void main(String[] args) throws Exception {
ConnectionFactory factory = initConnectionFactory();
Connection connection = factory.newConnection();
Channel channel1 = connection.createChannel();
Channel channel2 = connection.createChannel();
Channel channel3 = connection.createChannel();
QslConsumer qslConsumer1 = new QslConsumer(channel1);
QslConsumer qslConsumer2 = new QslConsumer(channel2);
QslConsumer qslConsumer3 = new QslConsumer(channel3);
String consumerTag1 = channel1.basicConsume(QUEUE_NAME, false, qslConsumer1);
String consumerTag2 = channel2.basicConsume(QUEUE_NAME, false, qslConsumer2);
String consumerTag3 = channel3.basicConsume(QUEUE_NAME, false, qslConsumer3);
System.out.println(Thread.currentThread().getName() + " 執行緒執行完畢,消費者["
+ Arrays.asList(consumerTag1, consumerTag2, consumerTag3) + "]已經就緒");
}
執行 main 方法後 Channel 與 Consumer 關係如下
既然兩種方式都可以實現多消費者,哪那種方式更好呢
Channel 與 Consumer 一對一繫結更好!
Channel 之間是執行緒安全的,同個 Channel 內非執行緒安全,所以同個 Channel 上同時處理多個消費者存在併發問題;另外 RabbitMQ 的訊息確認機制是基於Channel 的,如果一個 Channel 上繫結多個消費者,那麼訊息確認會變得複雜,非常容易導致訊息重複消費或丟失
也許你們會覺得 一對一
的繫結相較於 一對多
的繫結,存在資源浪費問題;確實是有這個問題,但我們要知道,Channel 是 Connection 中的一條獨立的雙向資料流通道,非常輕量級,相較於併發帶來的一系列問題而言,這點小小的資源浪費可以忽略不計了
消費者數量能不能配置化呢,當然可以,調整非常簡單
private static final int concurrency = 3;
public static void main(String[] args) throws Exception {
ConnectionFactory factory = initConnectionFactory();
Connection connection = factory.newConnection();
for (int i = 0; i < concurrency; i++) {
Channel channel = connection.createChannel();
QslConsumer qslConsumer = new QslConsumer(channel);
String consumerTag = channel.basicConsume(QUEUE_NAME, false, qslConsumer);
System.out.println("消費者:" + consumerTag + " 已經就緒");
}
}
concurrency
的值是從資料庫讀取,還是從配置檔案中獲取,就可以發揮你們的想象呢;如果依賴 Spring 的話,往往會用配置檔案的方式注入進來
消費者預取數
佇列 qsl.queue
沒有消費者的情況下,我們往佇列中新增 5 條訊息:我是訊息1 ~ 我是訊息5,然後調整下 handleDelivery
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, StandardCharsets.UTF_8);
System.out.println(consumerTag + " 收到訊息:" + message);
this.getChannel().basicAck(envelope.getDeliveryTag(), false);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
最後執行 main,控制檯輸出如下
大家注意看框住的那部分,5 條訊息被同一個消費者給消費了!5 條訊息為什麼不是負載均衡到 3 個消費者呢?這是因為消費者的 prefetch count
(即 預取數
)沒有設定
prefetch count 是消費者在接收訊息時,告訴 RabbitMQ 一次最多可以傳送多少條訊息給該消費者。預設情況下,這個值是 0,這意味著 RabbitMQ 會盡可能快地將訊息分發給消費者,而不考慮消費者當前的處理能力
再回過頭去看控制檯的輸出,是不是就能理解了?一旦某個消費者就緒,佇列中的 5 條訊息全部推給它了,後面就緒的 2 個消費者就沒有訊息可消費了;所以我們需要配置 prefetch count
以實現負載均衡,調整很簡單
private static final String QUEUE_NAME = "qsl.queue";
private static final int concurrency = 3;
private static final int prefetchCount = 1;
public static void main(String[] args) throws Exception {
ConnectionFactory factory = initConnectionFactory();
Connection connection = factory.newConnection();
for (int i = 0; i < concurrency; i++) {
Channel channel = connection.createChannel();
channel.basicQos(prefetchCount);
QslConsumer qslConsumer = new QslConsumer(channel);
String consumerTag = channel.basicConsume(QUEUE_NAME, false, qslConsumer);
System.out.println("消費者:" + consumerTag + " 已經就緒");
}
}
然後重複如上的測試,控制檯輸出如下
是不是實現了我們想要的 負載均衡
?
prefetch count 的設定需要根據實際的業務需求和消費者的處理能力進行調整;如果設定得太高,可能會導致記憶體佔用過多;如果設定得太低,則可能無法充分利用消費者的處理能力
其他完善
限於篇幅,我就只列舉幾個還待完善的點
- 目前只支援單個佇列,需要支援多個佇列
- 目錄消費邏輯單一固定,需要支援動態指定邏輯,不同的佇列對應不同的消費邏輯
- 消費者支援停止和重啟
- ...
關於這些點,我們下篇不見不散
總結
- Connection、Channel、Consumer 之間的關係需要理清楚
Connection 是 TCP 連線;Channel 是 Connection 中的雙向資料流通道;Channel 可以繫結多個 Consumer,但推薦一個 Channel 只繫結一個 Consumer
IO 多路複用
是網路程式設計中常用的技術,建議大家掌握
2. 基於 RabbitMQ Java Client 提供的 API,實現了訊息消費、多消費者以及負載均衡
沒有 Spring,我們照樣可以很優雅的消費 RabbitMQ 的訊息
本部落格參考楚門加速器p。轉載請註明出處!