0 前言
官網描述六類工作佇列模式:
- 簡單佇列模式:最簡單的工作佇列,一個訊息生產者,一個訊息消費者,一個佇列。另稱點對點模式
- 工作模式:一個訊息生產者,一個交換器,一個訊息佇列,多個消費者。也稱點對點模式
- 釋出/訂閱模式:無選擇接收訊息,一個訊息生產者,一個交換器,多個訊息佇列,多個消費者
- 路由模式:基於釋出/訂閱模式,有選擇的接收訊息,即透過 routing 路由進行匹配條件是否滿足接收訊息
- 主題模式:同樣是在釋出/訂閱模式的基礎上,根據主題匹配進行篩選是否接收訊息,比第四類更靈活
- RPC模式:擁有請求/回覆的。也就是有響應的,這是其它都沒的
1 簡單佇列模式
1 實現功能
一個生產者 P 傳送訊息到佇列 Q,一個消費者 C 接收:
Pro
Pro負責建立訊息佇列,併傳送訊息入列:
- 獲取連線
- 建立通道
- 建立佇列宣告
- 傳送訊息
- 關閉佇列
public class Producer {
private static final String QUEUE_NAME = "test_queue";
public static void main(String[] args) throws IOException, TimeoutException {
Connection newConnection = MQConnectionUtils.newConnection();
Channel channel = newConnection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
String msg = "我是生產者生成的訊息";
System.out.println("生產者傳送訊息:" + msg);
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
channel.close();
newConnection.close();
}
}
Con
- 獲取連線
- 獲取通道
- 監聽佇列
public class Customer {
private static final String QUEUE_NAME = "test_queue";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("002");
Connection newConnection = MQConnectionUtils.newConnection();
Channel channel = newConnection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
String msgString = new String(body, "UTF-8");
System.out.println("消費者獲取訊息:" + msgString);
}
};
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
建立vhost
2 工作佇列模式
將耗時的任務分發給多個消費者(工作者)。
主要解決:處理資源密集型任務,且還要等他完成。有了工作佇列,就可將具體的工作放到後面去做,將工作封裝為一個訊息,傳送到佇列中,一個工作程序就可取出訊息並完成工作。若啟動了多個工作程序,則工作就可在多個程序間共享。
工作佇列也稱公平性佇列模式,迴圈分發,若有兩個消費者,預設RabbitMQ按序將每條訊息發給下一個 Con,每個消費者獲得相同數量的訊息,即輪詢。
Pro
建立50個訊息
public class Producer2 {
private static final String QUEUE_NAME = "test_queue";
public static void main(String[] args) throws IOException, TimeoutException {
Connection newConnection = MQConnectionUtils.newConnection();
Channel channel = newConnection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/**保證一次只分發一次 限制傳送給同一個消費者 不得超過一條訊息 */
channel.basicQos(1);
for (int i = 1; i <= 50; i++) {
String msg = "生產者訊息_" + i;
System.out.println("生產者傳送訊息:" + msg);
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
}
channel.close();
newConnection.close();
}
}
Con
public class Customer2_1 {
private static final String QUEUE_NAME = "test_queue";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("001");
Connection newConnection = MQConnectionUtils.newConnection();
final Channel channel = newConnection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/** 保證一次只分發一次 限制傳送給同一個消費者 不得超過一條訊息 */
channel.basicQos(1);
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
String msgString = new String(body, "UTF-8");
System.out.println("消費者獲取訊息:" + msgString);
try {
Thread.sleep(1000);
} catch (Exception e) {
} finally {
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
}
}
迴圈分發
啟動生產者
啟動兩個消費者
Pro傳送了50條訊息進入佇列,而上方消費者啟動圖裡很明顯的看到輪詢的效果,就是每個消費者會分到相同的佇列任務。
公平分發
由於上方模擬的是非常簡單的訊息佇列的消費,假如有一些非常耗時的任務,某個消費者在緩慢地進行處理,而另一個消費者則空閒,顯然是非常消耗資源的。如一個1年的程式設計師,跟一個3年的程式設計師,分配相同的任務量,明顯3年的程式設計師處理起來更加得心應手,很快就無所事事了,但是3年的程式設計師拿著非常高的薪資!顯然3年的程式設計師應該承擔更多的責任,咋辦?
發生上述問題的原因是 RabbitMQ 收到訊息後就立即分發出去,而沒有確認各個工作者未返回確認的訊息數量,類似UDP,面向無連線。可用 basicQos,並將引數 prefetchCount 設為1,告訴 RabbitMQ 我每次值處理一條訊息,你要等我處理完了再分給我下一個。這樣 RabbitMQ 就不會輪流分發了,而是尋找空閒的工作者進行分發。
final Channel channel = newConnection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/** 保證一次只分發一次 限制傳送給同一個消費者 不得超過一條訊息 */
channel.basicQos(1);
訊息持久化
背景
上邊我們提到的公平分發是由消費者收取訊息時確認解決的,但是這裡面又會出現被 kill 的情況。
當有多個消費者同時收取訊息,且每個消費者在接收訊息的同時,還要處理其它的事情,且會消耗很長的時間。在此過程中可能會出現一些意外,比如訊息接收到一半的時候,一個消費者死掉了。
這種情況要使用訊息接收確認機制,可以執行上次當機的消費者沒有完成的事情。
但是在預設情況下,我們程式建立的訊息佇列以及存放在佇列裡面的訊息,都是非持久化的。當RabbitMQ死掉了或者重啟了,上次建立的佇列、訊息都不會儲存。咋辦?
引數配置
引數配置一:生產者建立佇列宣告時,修改第二個引數為 true
/**3.建立佇列宣告 */
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
引數配置二:生產者傳送訊息時,修改第三個引數為MessageProperties.PERSISTENT_TEXT_PLAIN
for (int i = 1; i <= 50; i++) {
String msg = "生產者訊息_" + i;
System.out.println("生產者傳送訊息:" + msg);
channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes());
}
小結
- 迴圈分發:消費者端在通道上開啟訊息應答機制,並確保能返回接收訊息的確認資訊,這樣可以保證消費者發生故障也不會丟失訊息
- 訊息持久化:伺服器端和客戶端都要指定佇列的持久化和訊息的持久化,這樣可以保證RabbitMQ重啟,佇列和訊息也不會丟失
- 公平分發:指定消費者接收的訊息個數,避免出現訊息均勻推送出現的資源不合理利用的問題
3 釋出訂閱模式
工作佇列模式是直接在生產者與消費者裡宣告好一個佇列,訊息就只會對應同型別的消費者。這種只處理同種型別的訊息有弊端。
3.1 案例
入口網站,使用者註冊完後一般都會傳送訊息通知使用者註冊結果。如在一個系統中,使用者註冊資訊有郵箱、手機號,在註冊完後會向郵箱和手機號都傳送註冊完成資訊。
利用 MQ 實現業務非同步處理,若用工作佇列,就宣告一個註冊資訊佇列。註冊完成後生產者向佇列提交一條註冊資料,消費者取出資料同時向郵箱以及手機號傳送兩條訊息。但實際上郵箱和手機號資訊傳送實際上是不同的業務邏輯,不應放在一塊處理。
這時就可利用釋出/訂閱模式將訊息傳送到轉換機(EXCHANGE),宣告兩個不同的佇列(郵箱、手機),並繫結到交換機。這樣生產者只需要釋出一次訊息,兩個佇列都會接收到訊息發給對應的消費者:
只需簡單的將佇列繫結到交換機。一個傳送到交換機的訊息都會被轉發到與該交換機繫結的所有佇列。就像子網廣播,每臺子網內的主機都獲得一份複製的訊息。
3.2 啥是釋出訂閱模式
可將訊息傳送給不同型別的消費者。即釋出一次,消費多個:
X表示交換機、紅色表示佇列。
展示郵件、簡訊的例子,透過繫結到一個交換機,但是
3.3 實戰
public class ProducerFanout {
private static final String EXCHANGE_NAME = "fanout_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
/** 1.建立新的連線 */
Connection connection = MQConnectionUtils.newConnection();
/** 2.建立通道 */
Channel channel = connection.createChannel();
/** 3.繫結的交換機 引數1互動機名稱 引數2 exchange型別 */
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
/** 4.傳送訊息 */
for (int i = 0; i < 10; i++)
{
String message = "使用者註冊訊息:" + i;
System.out.println("[send]:" + message);
// 第二個引數為空類似於表示全域性廣播,只要繫結到該佇列上的消費者理論上是都可收到
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("utf-8"));
try {
Thread.sleep(5 * i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/** 5.關閉通道、連線 */
channel.close();
connection.close();
/** 注意:如果消費沒有繫結交換機和佇列,則訊息會丟失 */
}
}
郵件消費者
public class ConsumerEmailFanout {
private static final String QUEUE_NAME = "consumerFanout_email";
private static final String EXCHANGE_NAME = "fanout_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("郵件消費者啟動");
/* 1.建立新的連線 */
Connection connection = MQConnectionUtils.newConnection();
/* 2.建立通道 */
Channel channel = connection.createChannel();
/* 3.消費者關聯佇列 */
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/* 4.消費者繫結交換機 引數1 佇列 引數2交換機 引數3 routingKey */
// 第三個引數置為空時,可以接收到生產者所有的訊息(生產者 routingKey 引數為空時)
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("消費者獲取生產者訊息:" + msg);
}
};
/* 5.消費者監聽佇列訊息 */
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
簡訊消費者
public class ConsumerSMSFanout {
private static final String QUEUE_NAME = "ConsumerFanout_sms";
private static final String EXCHANGE_NAME = "fanout_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("簡訊消費者啟動");
/* 1.建立新的連線 */
Connection connection = MQConnectionUtils.newConnection();
/* 2.建立通道 */
Channel channel = connection.createChannel();
/* 3.消費者關聯佇列 */
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/* 4.消費者繫結交換機 引數1 佇列 引數2交換機 引數3 routingKey */
// 第三個引數置為空時,可接收到生產者所有的訊息(生產者 routingKey 引數為空時)
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("消費者獲取生產者訊息:" + msg);
}
};
/* 5.消費者監聽佇列訊息 */
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
執行
先執行兩個con,再執行pro。如沒有提前將佇列繫結到交換機,直接執行pro,訊息是不會發到任何佇列裡的。
生產者
簡訊消費者
郵件消費者
小結
相比工作模式,釋出訂閱模式引入了交換機,型別上更靈活。
pro不是直接操作佇列,而是將資料發給交換機,由交換機將資料發給與之繫結的佇列。從不加特定引數的執行結果中可以看到,兩種型別的消費者(email,sms)都收到相同數量訊息。
必須宣告交換機,並設定模式:channel.exchangeDeclare(EXCHANGE_NAME, "fanout"),fanout 指分發模式(將每一條訊息都傳送到與交換機繫結的佇列)
佇列必須繫結交換機:channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
生產者傳送訊息到交換機,多個消費者宣告多個佇列,與交換機進行繫結,佇列中的訊息可以被所有消費者消費,類似QQ群訊息
4 路由模式
就是釋出訂閱模式(Publish/Subscribe Pattern)中的直連交換機(Direct Exchange)。一種基於路由鍵(Routing Key)來路由訊息的模式。在這種模式下,生產者傳送訊息時會指定一個路由鍵,交換機會根據這個路由鍵將訊息路由到與之匹配的佇列。
Pro
使用 channel.basicPublish
方法傳送訊息,並指定交換機名稱和路由鍵。交換機會根據路由鍵將訊息路由到與之匹配的佇列。
Con
在消費者程式碼中,我們宣告瞭一個直接交換機(direct
型別),並繫結了一個佇列。在繫結佇列時,我們使用 channel.queueBind
方法,並指定交換機名稱、佇列名稱和路由鍵。交換機會根據路由鍵將訊息路由到與之匹配的佇列。
特點
- 路由鍵匹配:訊息的路由鍵必須與佇列繫結的路由鍵完全匹配,才能將訊息路由到該佇列。
- 直接交換機:直接交換機根據路由鍵進行精確匹配,適用於需要精確控制訊息路由的場景。
透過這種方式,路由模式可以實現基於路由鍵的精確訊息路由,適用於需要將訊息傳送到特定佇列的場景。
5 主題模式
屬於釋出訂閱模式的TopicExchange(主題交換機)。Queue 透過 routing key 繫結到 TopicExchange,當訊息到達TopicExchange後,TopicEkchange 根據訊息的 routing key 將訊息路由到一個或者多個Queue。
關注我,緊跟本系列專欄文章,咱們下篇再續!
作者簡介:魔都架構師,多家大廠後端一線研發經驗,在分散式系統設計、資料平臺架構和AI應用開發等領域都有豐富實踐經驗。
各大技術社群頭部專家博主。具有豐富的引領團隊經驗,深厚業務架構和解決方案的積累。
負責:
- 中央/分銷預訂系統效能最佳化
- 活動&券等營銷中臺建設
- 交易平臺及資料中臺等架構和開發設計
- 車聯網核心平臺-物聯網連線平臺、大資料平臺架構設計及最佳化
- LLM Agent應用開發
- 區塊鏈應用開發
- 大資料開發挖掘經驗
- 推薦系統專案
目前主攻市級軟體專案設計、構建服務全社會的應用系統。
參考:
- 程式設計嚴選網
本文由部落格一文多發平臺 OpenWrite 釋出!