延遲隊列可以實現消息在投遞到
Exchange
之后,經過一定的時間之后再投遞到相應的Queue
。再被消費者監聽消費。
即:生產者投遞的消息經過一段時間之后再被消費者消費。
- 業務場景:訂單在30分鐘內還未支付則自動取消。
該業務的其他實現方案:
- 使用
Redis
,設置過期時間,監聽過期事件。 - 使用RabbitMQ的過期隊列與死信隊列,設置消息的存活時間,在設置的時間內未被消費,即會投遞到死信隊列,我們監聽死信隊列即可。可參考上一篇文章RabbitMQ死信隊列在SpringBoot中的使用。
使用RabbitMQ延遲隊列實現:
# 安裝延遲隊列插件:
- RabbitMQ插件列表: https://www.rabbitmq.com/community-plugins.html
- 延遲隊列插件下載地址: https://github.com/rabbitmq/rabbitmq-delayed-message-exchange
- Mac下的插件路徑為
/usr/local/Cellar/rabbitmq/3.7.15/plugins
- 安裝并啟用
- 重啟RabbitMQ
# 業務相關代碼編寫
- 訂單實體(僅保留相關字段)
- 訂單狀態枚舉(僅保留相關狀態)
- OrderMapper
/**
* @author futao
* @date 2020/4/14.
*/
public interface OrderMapper extends BaseMapper<Order> {
}
- 模擬下定的接口
OrderController
為了簡單起見,省略了Service層.
# RabbitMQ相關代碼編寫
- 配置文件
spring:
rabbitmq:
host: localhost
port: 5672
username: futao
password: 123456789
virtual-host: delay-vh
connection-timeout: 15000
# 發送確認
publisher-confirms: true
# 路由失敗回調
publisher-returns: true
template:
# 必須設置成true 消息路由失敗通知監聽者,而不是將消息丟棄
mandatory: true
listener:
simple:
# 每次從RabbitMQ獲取的消息數量
prefetch: 1
default-requeue-rejected: false
# 每個隊列啟動的消費者數量
concurrency: 1
# 每個隊列最大的消費者數量
max-concurrency: 1
# 手動簽收ACK
acknowledge-mode: manual
app:
rabbitmq:
# 延遲時長設置
delay:
order: 10S
# 隊列定義
queue:
order:
delay: order-delay-queue
# 交換機定義
exchange:
order:
delay: order-delay-exchange
- 延遲交換機,隊列定義與綁定
/**
* 隊列,交換機定義與綁定
* 延遲隊列插件`rabbitmq-delayed-message-exchange`下載地址 https://github.com/rabbitmq/rabbitmq-delayed-message-exchange
*
* @author futao
* @date 2020/4/10.
*/
@Configuration
public class Declare {
/**
* 訂單隊列 - 接收延遲投遞的訂單
*
* @param orderQueue 訂單隊列名稱
* @return
*/
@Bean
public Queue orderDelayQueue(@Value("${app.rabbitmq.queue.order.delay}") String orderQueue) {
return QueueBuilder
.durable(orderQueue)
.build();
}
/**
* 訂單交換機-延遲交換機 - 消息延遲一定時間之后再投遞到綁定的隊列
*
* @param orderExchange 訂單延遲交換機
* @return
*/
@Bean
public Exchange orderDelayExchange(@Value("${app.rabbitmq.exchange.order.delay}") String orderExchange) {
Map<String, Object> args = new HashMap<>(1);
args.put("x-delayed-type", "topic");
return new CustomExchange(orderExchange, "x-delayed-message", true, false, args);
}
/**
* 訂單隊列-交換機 綁定
*
* @param orderQueue 訂單隊列
* @param orderDelayExchange 訂單交換機
* @return
*/
@Bean
public Binding orderBinding(Queue orderDelayQueue, Exchange orderDelayExchange) {
return BindingBuilder
.bind(orderDelayQueue)
.to(orderDelayExchange)
.with("order.delay.*")
.noargs();
}
}
可以看出隊列就是普通的隊列。重點在交換機的設定上。聲明延遲交換機需要設置參數x-delayed-type
,值為交換機類型,可以是fanout
,topic
,direct
。并且設置交換機的type為x-delayed-message
。
- 定義完成后可以啟動SpringBoot應用程序,在RabbitMQ管理后臺查看Exchange和Queue。
可以看到,除了默認的交換機,SpringBoot已經幫我們創建好了延遲交換機order-delay-exchange
,并且此時messages delayed為0,因為我們還未向交換機投遞消息。
- 可以繼續查看交換機的路由類型與綁定的隊列
-
隊列為普通的隊列
Queue 回到代碼中,定義消息生產者
在消息投遞之前為每條消息都設置了延遲時長setDelay()
。
調用消費者的代碼在上面OrderController
中,下定之后,訂單數據落庫,并且向MQ中投遞延遲消息。可以回頭看看。
- 消費者-監聽過期的訂單信息,并且將DB中相應的訂單設置為已過期。
為了方便查看到延遲投遞的效果,我在消息投遞和接收處都打印了日志,測試時可以看到消息投遞和消息的時間間隔。
# 測試
- 把訂單過期時長設置為10S
app:
rabbitmq:
delay:
order: 10S
- 下定
可以看到,打印出了投遞日志,訂單主鍵為666ae86aabe2a1b3120b34bb5f447bbe
的訂單在2020-04-14 22:22:04.307
進行了投遞,此時數據庫中該訂單的狀態為100
,待支付。
- 此時查看Exchange詳情可以發現,
messages delayed:1
,即目前有一條消息處于延遲狀態。
- 等待10S后。
可以看到OrderConsumer
在10S后2020-04-14 22:22:14.320
接收到了主鍵為666ae86aabe2a1b3120b34bb5f447bbe
的訂單消息。距離投遞時間2020-04-14 22:22:04.307
為10S。此時查看DB中訂單狀態:
訂單狀態為200
已過期,且過期時間為2020-04-14 22:22:14
- 達到了訂單在我們指定的時間后過期。
- 再測試幾條一分鐘的場景
app:
rabbitmq:
delay:
order: 1M
消息都在延遲1分鐘后投遞到了隊列-消費者。
建議收藏,當然我只是建議。
# 嚴重風險提示:
在實際業務使用中,如果消費者的消費能力比較低下,會存在已經過期的消息阻塞積壓在隊列,無法在指定的時間內過期,導致業務出現異常。
實際上,按照我們業務意圖,隊里Queue里是不應該有大量消息存在的,因為投遞到過期隊列的消息已經是過期了的,應該立即被消費掉。
- 進行測試:
為了降低消費者的消費能力,進行如下處理:- 設置消費者的最大并發數為1,并進行手動簽收。
listener:
simple:
# 每個隊列啟動的消費者數量
concurrency: 1
# 每個隊列最大的消費者數量
max-concurrency: 1
acknowledge-mode: manual
app:
rabbitmq:
delay:
# 訂單過期時間為1分鐘
order: 1M
-
消費者在處理消息時休眠5S
sleep 向MQ投遞兩條消息,預期兩條消息都在1分鐘后正常過期。
結果(去除了無關信息):
2020-04-15 20:18:05.269 OrderSender : 訂單[d6fd965b11f8db0fafb762d305db83b0]投遞到MQ
2020-04-15 20:18:05.765 OrderSender : 訂單[77ceb7f1bfbbcaf627224ac75e96b0e5]投遞到MQ
2020-04-15 20:19:05.279 OrderConsumer : 消費者接收到延遲訂單[d6fd965b11f8db0fafb762d305db83b0]
2020-04-15 20:19:15.316 OrderConsumer : 訂單業務處理結束.....進行消息ack簽收
2020-04-15 20:19:15.318 OrderConsumer : 消費者接收到延遲訂單[77ceb7f1bfbbcaf627224ac75e96b0e5]
2020-04-15 20:19:25.330 OrderConsumer : 訂單業務處理結束.....進行消息ack簽收
第一個訂單d6fd965b11f8db0fafb762d305db83b0
投遞時間為2020-04-15 20:18:05.269
。1分鐘后2020-04-15 20:19:05.279
接收到了通知,并且處理了10S后進行了簽收ack。
第二個訂單77ceb7f1bfbbcaf627224ac75e96b0e5
投遞時間為2020-04-15 20:18:05.765
。1分鐘過后并沒有收到通知,而是在第一個訂單處理完畢之后,2020-04-15 20:19:15.318
才收到了通知,比預期的時間長了10秒,實際延遲時間為1分鐘+10秒。出現了業務異常。
- 導致這個問題的原因就是消費者無法及時消費消息并更新訂單狀態。所以我們在進行開發時,需要考慮實際的數據量大小,消費者消費能力。及時關注隊列消息積壓情況,靈活調整消費者并發數量,優化消費者代碼,提高消費者消費能力。
# 系列文章
任何技術的使用都不可生搬硬套,需要結合自己實際的業務場景進行相應的調整優化。在平時的工作中應該多關注程序在實際的運行過程中的結果是否符合我們的預期
本文涉及的源代碼:https://github.com/FutaoSmile/springboot-learn-integration/releases/tag/v_rabbitmq_delay_queue