使用RabbitMQ實現未支付訂單在30分鐘后自動過期

延遲隊列可以實現消息在投遞到Exchange之后,經過一定的時間之后再投遞到相應的Queue。再被消費者監聽消費。
即:生產者投遞的消息經過一段時間之后再被消費者消費。

  • 業務場景:訂單在30分鐘內還未支付則自動取消。

該業務的其他實現方案:

  • 使用Redis,設置過期時間,監聽過期事件。
  • 使用RabbitMQ的過期隊列與死信隊列,設置消息的存活時間,在設置的時間內未被消費,即會投遞到死信隊列,我們監聽死信隊列即可。可參考上一篇文章RabbitMQ死信隊列在SpringBoot中的使用

使用RabbitMQ延遲隊列實現:

# 安裝延遲隊列插件:

plugin
  • 重啟RabbitMQ

# 業務相關代碼編寫

  • 訂單實體(僅保留相關字段)
OrderEntity
  • 訂單狀態枚舉(僅保留相關狀態)
image.png
  • OrderMapper
/**
 * @author futao
 * @date 2020/4/14.
 */
public interface OrderMapper extends BaseMapper<Order> {
}
  • 模擬下定的接口OrderController
    為了簡單起見,省略了Service層.
OrderController

# 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。
delay-exchange

可以看到,除了默認的交換機,SpringBoot已經幫我們創建好了延遲交換機order-delay-exchange,并且此時messages delayed為0,因為我們還未向交換機投遞消息。

  • 可以繼續查看交換機的路由類型與綁定的隊列
ExchangeDetail
  • 隊列為普通的隊列


    Queue
  • 回到代碼中,定義消息生產者

Orderender

在消息投遞之前為每條消息都設置了延遲時長setDelay()
調用消費者的代碼在上面OrderController中,下定之后,訂單數據落庫,并且向MQ中投遞延遲消息。可以回頭看看。

  • 消費者-監聽過期的訂單信息,并且將DB中相應的訂單設置為已過期。
OrderConsumer

為了方便查看到延遲投遞的效果,我在消息投遞和接收處都打印了日志,測試時可以看到消息投遞和消息的時間間隔。

# 測試

  • 把訂單過期時長設置為10S
app:
  rabbitmq:
    delay:
      order: 10S
  • 下定
log
DB100

可以看到,打印出了投遞日志,訂單主鍵為666ae86aabe2a1b3120b34bb5f447bbe的訂單在2020-04-14 22:22:04.307進行了投遞,此時數據庫中該訂單的狀態為100,待支付。

  • 此時查看Exchange詳情可以發現,messages delayed:1,即目前有一條消息處于延遲狀態。
ExchangeDetail
  • 等待10S后。
log

可以看到OrderConsumer在10S后2020-04-14 22:22:14.320接收到了主鍵為666ae86aabe2a1b3120b34bb5f447bbe的訂單消息。距離投遞時間2020-04-14 22:22:04.307為10S。此時查看DB中訂單狀態:

DB

訂單狀態為200已過期,且過期時間為2020-04-14 22:22:14

  • 達到了訂單在我們指定的時間后過期。

  • 再測試幾條一分鐘的場景
app:
  rabbitmq:
    delay:
      order: 1M
Exchange
log

消息都在延遲1分鐘后投遞到了隊列-消費者。

建議收藏,當然我只是建議。



# 嚴重風險提示:

在實際業務使用中,如果消費者的消費能力比較低下,會存在已經過期的消息阻塞積壓在隊列,無法在指定的時間內過期,導致業務出現異常。
實際上,按照我們業務意圖,隊里Queue里是不應該有大量消息存在的,因為投遞到過期隊列的消息已經是過期了的,應該立即被消費掉。

  • 進行測試:
    為了降低消費者的消費能力,進行如下處理:
    1. 設置消費者的最大并發數為1,并進行手動簽收。
    listener:
      simple:
        # 每個隊列啟動的消費者數量
        concurrency: 1
        # 每個隊列最大的消費者數量
        max-concurrency: 1
        acknowledge-mode: manual

app:
  rabbitmq:
    delay:
      # 訂單過期時間為1分鐘
      order: 1M
  1. 消費者在處理消息時休眠5S


    sleep
  2. 向MQ投遞兩條消息,預期兩條消息都在1分鐘后正常過期。

  3. 結果(去除了無關信息):

result
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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容