使用RabbitMQ的死信隊列實現延遲消息

使用場景

  • 訂單下單30分鐘後,如果用戶沒有付款,則系統自動取消訂單。
  • 會議開始前10分鐘,推送消息提醒用戶。
  • 自定義某個操作的執行時間,如果設置文章在明早9點發布。

除了上一篇《使用RabbitMQ插件實現延遲隊列》外,使用死信隊列也是一種方案.

  • Time To Live:可以在發送消息時設置過期時間,也可以設置整個隊列的過期時間,如果兩個同時設置已最早過期時間爲準。
  • Dead Letter Exchanges:可以通過綁定隊列的死信交換器來實現死信隊列。
x-dead-letter-exchange:綁定死信交換器(其實也是普通交換器,與類型無關)
x-dead-letter-routing-key:綁定死信隊列的路由鍵(可選)
x-message-ttl:綁定隊列消息的過期時間(可選)

 死信隊列設計思路:

生產者 --> 消息 --> 交換機 --> 隊列 --> 變成死信 --> DLX交換機 -->隊列 --> 消費者

進入消息隊列:
1. 消息被拒絕,並且requeue= false
2. 消息ttl過期
3. 隊列達到最大的長度

做延遲隊列需要創建一個沒有消費者的隊列,用來存儲消息。然後創建一個真正的消費隊列,用來做具體的業務邏輯。當帶有TTL的消息到達綁定死信交換器的隊列,因爲沒有消費者所以會一直等到消息過期,然後消息被投遞到死信隊列也就是真正的消費隊列。
具體代碼:
package com.lyt.rabbitmq.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

/**
 * @Description 利用死信隊列和過期時間模擬延遲隊列,沒有消費者,所以不能用註解形式
 * Time To Live(TTL)
 * 1. 可以在發送消息時設置過期時間(message.getMessageProperties().setExpiration("5000");)
 * 2. 也可以設置整個隊列的過期時間(args.put("x-message-ttl",10000);)
 * 3. 如果兩個同時設置已最早過期時間爲準
 * Dead Letter Exchanges(DLX)
 * @Date 2019-03-10 10:25:30
 */
@Component
public class MQDelayConfig {

    /**
     * @Description 定義支付交換器
     * @Author lyt
     * @Date 2021-04-02 14:39:31
     */
    @Bean
    private DirectExchange directPayExchange() {
        return new DirectExchange("direct.pay.exchange");
    }

    /**
     * @Description 定義支付隊列 綁定死信隊列(其實是綁定的交換器,然後通過交換器路由鍵綁定隊列) 設置過期時間
     * @Author lyt
     * @Date 2021-04-02 14:40:24
     */
    @Bean
    private Queue directPayQueue() {
        Map<String, Object> args = new HashMap<>(3);
        //聲明死信交換器
        args.put("x-dead-letter-exchange", "direct.delay.exchange");
        //聲明死信路由鍵
        args.put("x-dead-letter-routing-key", "DelayKey");
        //聲明隊列消息過期時間
        args.put("x-message-ttl", 10000);
        return new Queue("direct.pay.queue", true, false, false, args);
    }

    /**
     * @Description 定義支付綁定
     * @Author lyt
     * @Date 2021-04-02 14:46:10
     */
    @Bean
    private Binding bindingOrderDirect() {
        return BindingBuilder.bind(directPayQueue()).to(directPayExchange()).with("OrderPay");
    }
}

帶有過期時間且綁定死信交換器的隊列

生產者,爲消息設置過期時間setExpiration("15000");

/**
 * @Description 支付隊列、綁定死信隊列,測試消息延遲功能
 * @Author lyt
 * @Date 2021-04-02 14:07:25
 */
@RequestMapping(value = "/directDelayMQ", method = {RequestMethod.GET})
public List<User> directDelayMQ() {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    List<User> users = userService.getUserList(null);
    for (User user : users) {
        CorrelationData correlationData = new CorrelationData(String.valueOf(user.getId()));
        rabbitTemplate.convertAndSend("direct.pay.exchange", "OrderPay", user,
                message -> {
                    // 設置5秒過期
                    message.getMessageProperties().setExpiration("15000");
                    return message;
                },
                correlationData);
        System.out.println(user.getName() + ":" + sdf.format(new Date()));
    }
    return users;
}

消費者,聲明真正消費的隊列、交換器、綁定

/**
 * @Description 延遲隊列
 * @Author lyt
 * @Date 2021-04-04 16:34:28
 */
@RabbitListener(bindings = {@QueueBinding(value = @Queue(value = "direct.delay.queue"), exchange = @Exchange(value = "direct.delay.exchange"), key = {"DelayKey"})})
public void getDLMessage(User user, Channel channel, Message message) throws InterruptedException, IOException {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    // 模擬執行任務
    System.out.println("這是延遲隊列消費:" + user.getName() + ":" + sdf.format(new Date()));
    channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}

思考: 如果先放入一條A消息過期時間是10秒,再放入一個b消息過期時間是5秒,那延遲隊列是否可以先消費b消息?

答案是否定的,因爲隊列就會遵循先進先出的規則,b消息會等a消息過期後,一起消費,這就是所謂的隊列阻塞。由這個問題可以用我們之前介紹的插件方式解決。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章