1. 工作隊列或者競爭消費者模式
上篇基本消息模型中是從一個命名隊列中發送並接受消息。在這裏,將創建一個工作隊列,在多個工作者之間分配任務。
工作隊列,又稱任務隊列。主要思想就是避免執行資源密集型任務時,必須等待它執行完成。相反我們稍後完成任務,我們將任務封裝爲消息並將其發送到隊列。 在後臺運行的工作進程將獲取任務並最終執行作業。當你運行許多工人時,任務將在他們之間共享,但是一個消息只能被一個消費者獲取。
這個概念在Web應用程序中特別有用,因爲在短的HTTP請求窗口中無法處理複雜的任務。
接下來我們來模擬這個流程:
P:生產者:任務的發佈者
C1:消費者,領取任務並且完成任務,假設完成速度較快
C2:消費者2:領取任務並完成任務,假設完成速度慢
那麼如何避免消息堆積?
1) 採用workqueue,多個消費者監聽同一隊列。
2)接收到消息以後,而是通過線程池,異步消費。
2. 生產者
發送50條消息。
// 生產者
public class Send {
private final static String QUEUE_NAME = "test_work_queue";
public static void main(String[] argv) throws Exception {
// 獲取到連接
Connection connection = ConnectionUtil.getConnection();
// 獲取通道
Channel channel = connection.createChannel();
// 聲明隊列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 循環發佈任務
for (int i = 0; i < 50; i++) {
// 消息內容
String message = "task .. " + i;
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
Thread.sleep(i * 2);
}
// 關閉通道和連接
channel.close();
connection.close();
}
}
3. 消費者1
// 消費者1
public class Recv {
private final static String QUEUE_NAME = "test_work_queue";
public static void main(String[] argv) throws Exception {
// 獲取到連接
Connection connection = ConnectionUtil.getConnection();
// 獲取通道
final Channel channel = connection.createChannel();
// 聲明隊列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 設置每個消費者同時只能處理一條消息
channel.basicQos(1);
// 定義隊列的消費者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 獲取消息,並且處理,這個方法類似事件監聽,如果有消息的時候,會被自動調用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
// body 即消息體
String msg = new String(body);
System.out.println(" [消費者1] received : " + msg + "!");
try {
// 模擬完成任務的耗時:1000ms
Thread.sleep(1000);
} catch (InterruptedException e) {
}
// 手動ACK
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 監聽隊列。
channel.basicConsume(QUEUE_NAME, false, consumer);
}
}
注意: 消費者1沒處理一條消息延時了一秒時間。
3. 消費者2
//消費者2
public class Recv2 {
private final static String QUEUE_NAME = "test_work_queue";
public static void main(String[] argv) throws Exception {
// 獲取到連接
Connection connection = ConnectionUtil.getConnection();
// 獲取通道
final Channel channel = connection.createChannel();
// 聲明隊列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 設置每個消費者同時只能處理一條消息
channel.basicQos(1);
// 定義隊列的消費者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 獲取消息,並且處理,這個方法類似事件監聽,如果有消息的時候,會被自動調用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
// body 即消息體
String msg = new String(body);
System.out.println(" [消費者2] received : " + msg + "!");
// 手動ACK
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 監聽隊列。
channel.basicConsume(QUEUE_NAME, false, consumer);
}
}
與消費者1基本類似,就是沒有設置消費耗時時間。
這裏是模擬有些消費者快,有些比較慢。
接下來,兩個消費者一同啓動,然後讓生產者發送50條消息:
0-49
可以發現,兩個消費者各自消費了25條消息,而且各不相同,這就實現了任務的分發。
但還有一個現象就是消費者2早早就完成了任務,消費者還在每隔一秒消費一個消息(人工仿造業務處理慢的場景) ,這樣就造成了性能的浪費。如何解決?
4. 能者多勞
- 消費者1比消費者2的效率要低,一次任務的耗時較長
- 然而兩人最終消費的消息數量是一樣的
- 消費者2大量時間處於空閒狀態,消費者1一直忙碌
現在的狀態屬於是把任務平均分配,正確的做法應該是消費越快的人,消費的越多。
怎麼實現呢?
可以使用basicQos方法和prefetchCount = 1設置。 這告訴RabbitMQ一次不要向工作人員發送多於一條消息。 或者換句話說,不要向工作人員發送新消息,直到它處理並確認了前一個消息。 相反,它會將其分派給不是仍然忙碌的下一個工作人員。
消費者一和消費者二分別做上述的設置後,再次執行發送50條消息,看2個消費者的消費情況:
可以看到配置有了明顯效果。