SpringBoot + RabbitMQ 做延時隊列

SpringBoot + RabbitMQ 做延時隊列

グ〞夜微涼 ~

一、前言

延遲隊列的使用場景:
  1.未按時支付的訂單,30分鐘過期之後取消訂單;
  2.給活躍度比較低的用戶間隔N天之後推送消息,提高活躍度;
  3.過1分鐘給新註冊會員的用戶,發送註冊郵件等。

實現延遲隊列的方式有兩種:

通過消息過期後進入死信交換器,再由交換器轉發到延遲消費隊列,實現延遲功能;
使用rabbitmq-delayed-message-exchange插件實現延遲功能;
注意: 延遲插件rabbitmq-delayed-message-exchange是在RabbitMQ 3.5.7及以上的版本才支持的,依賴Erlang/OPT 18.0及以上運行環境。
由於使用死信交換器相對曲折,本文重點介紹第二種方式,使用rabbitmq-delayed-message-exchange插件完成延遲隊列的功能。

AMQP協議和RabbitMQ隊列本身沒有直接支持延遲隊列功能,但是我們可以通過RabbitMQ的兩個特性來曲線實現延遲隊列:

特性一:Time To Live(TTL)

RabbitMQ可以針對Queue設置x-expires 或者 針對Message設置 x-message-ttl,來控制消息的生存時間,如果超時(兩者同時設置以最先到期的時間爲準),則消息變爲dead letter(死信)
RabbitMQ針對隊列中的消息過期時間有兩種方法可以設置。
A: 通過隊列屬性設置,隊列中所有消息都有相同的過期時間。
B: 對消息進行單獨設置,每條消息TTL可以不同。

如果同時使用,則消息的過期時間以兩者之間TTL較小的那個數值爲準。消息在隊列的生存時間一旦超過設置的TTL值,就成爲dead letter

特性二:Dead Letter Exchanges(DLX)

RabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key(可選)兩個參數,如果隊列內出現了dead letter,則按照這兩個參數重新路由轉發到指定的隊列。
x-dead-letter-exchange:出現dead letter之後將dead letter重新發送到指定exchange
x-dead-letter-routing-key:出現dead letter之後將dead letter重新按照指定的routing-key發送
隊列出現dead letter的情況有:
消息或者隊列的TTL過期
隊列達到最大長度
消息被消費端拒絕(basic.reject or basic.nack)並且requeue=false

二、安裝延遲插件

1.1 下載插件

下載和安裝詳細步驟

SpringBoot整合RabbitMQ

創建一個springBoot項目![

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

在 pom.xml 中添加 spring-boot-starter-amqp的依賴

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

在 application.yml文件中配置rabbitmq相關內容

spring:
 rabbitmq:
   host: 127.0.0.1
   port: 5672
   username: guest
   password: guest
   listener:
   direct:
     acknowledge-mode: manual
   simple:
     acknowledge-mode: manual

具體編碼實現

1.配置隊列

package com.hmg.rabbitmq.config;

import lombok.extern.log4j.Log4j2;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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



/**
 * @title rabbitmq配置類
 * @auther 吊炸天
 * @date 2019/12/18 21:28
 */
@Configuration
@Log4j2
public class DelayRabbitConfig {

    /**
     * 延遲隊列 TTL 名稱
     */
    private static final String ORDER_DELAY_QUEUE = "user.order.delay.queue";
    /**
     * DLX,dead letter發送到的 exchange
     * 延時消息就是發送到該交換機的
     */
    public static final String ORDER_DELAY_EXCHANGE = "user.order.delay.exchange";
    /**
     * routing key 名稱
     * 具體消息發送在該 routingKey 的
     */
    public static final String ORDER_DELAY_ROUTING_KEY = "order_delay";

    public static final String ORDER_QUEUE_NAME = "user.order.queue";
    public static final String ORDER_EXCHANGE_NAME = "user.order.exchange";
    public static final String ORDER_ROUTING_KEY = "order";

    /**
     * 延遲隊列配置
     * <p>
     * 1、params.put("x-message-ttl", 5 * 1000);
     * 第一種方式是直接設置 Queue 延遲時間 但如果直接給隊列設置過期時間,這種做法不是很靈活,(當然二者是兼容的,默認是時間小的優先)
     * 2、rabbitTemplate.convertAndSend(book, message -> {
     * message.getMessageProperties().setExpiration(2 * 1000 + "");
     * return message;
     * });
     * 第二種就是每次發送消息動態設置延遲時間,這樣我們可以靈活控制
     **/
    @Bean
    public Queue delayOrderQueue() {
        Map<String, Object> params = new HashMap<>();
        // x-dead-letter-exchange 聲明瞭隊列裏的死信轉發到的DLX名稱,
        params.put("x-dead-letter-exchange", ORDER_EXCHANGE_NAME);
        // x-dead-letter-routing-key 聲明瞭這些死信在轉發時攜帶的 routing-key 名稱。
        params.put("x-dead-letter-routing-key", ORDER_ROUTING_KEY);
        return new Queue(ORDER_DELAY_QUEUE, true, false, false, params);
    }

    /**
     * 需要將一個隊列綁定到交換機上,要求該消息與一個特定的路由鍵完全匹配。
     * 這是一個完整的匹配。如果一個隊列綁定到該交換機上要求路由鍵 “dog”,則只有被標記爲“dog”的消息才被轉發,
     * 不會轉發dog.puppy,也不會轉發dog.guard,只會轉發dog。
     *
     * @return DirectExchange
     */
    @Bean
    public DirectExchange orderDelayExchange() {
        return new DirectExchange(ORDER_DELAY_EXCHANGE);
    }

    @Bean
    public Binding dlxBinding() {
        return BindingBuilder.bind(delayOrderQueue()).to(orderDelayExchange()).with(ORDER_DELAY_ROUTING_KEY);
    }

    @Bean
    public Queue orderQueue() {
        return new Queue(ORDER_QUEUE_NAME, true);
    }

    /**
     * 將路由鍵和某模式進行匹配。此時隊列需要綁定要一個模式上。
     * 符號“#”匹配一個或多個詞,符號“*”匹配不多不少一個詞。因此“audit.#”能夠匹配到“audit.irs.corporate”,但是“audit.*” 只會匹配到“audit.irs”。
     **/
    @Bean
    public TopicExchange orderTopicExchange() {
        return new TopicExchange(ORDER_EXCHANGE_NAME);
    }

    @Bean
    public Binding orderBinding() {
        // TODO 如果要讓延遲隊列之間有關聯,這裏的 routingKey 和 綁定的交換機很關鍵
        return BindingBuilder.bind(orderQueue()).to(orderTopicExchange()).with(ORDER_ROUTING_KEY);
    }

}

2.創建一個Order實體類

package com.hmg.rabbitmq.entity;

import lombok.Data;

import java.io.Serializable;

/**
 * @auther 吊炸天
 * @date 2019/12/18 21:43
 */
@Data
public class Order implements Serializable {

    private static final long serialVersionUID = -2221214252163879885L;

    /**
     * 訂單id
     */
    private String orderId;

    /**
     * 訂單狀態 0:未支付,1:已支付,2:訂單已取消
     */
    private Integer orderStatus;

    /**
     * 訂單名字
     */
    private String orderName;
}

3.接收者

package com.hmg.rabbitmq.config;

import com.hmg.rabbitmq.entity.Order;

import com.rabbitmq.client.Channel;
import lombok.extern.log4j.Log4j2;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * @auther 吊炸天
 * @date 2019/12/18 21:46
 */
@Component
@Log4j2
public class DelayReceiver {

    @RabbitListener(queues = {DelayRabbitConfig.ORDER_QUEUE_NAME})
    public void orderDelayQueue(Order order, Message message, Channel channel) {
        log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
        log.info("【orderDelayQueue 監聽的消息】 - 【消費時間】 - [{}]- 【訂單內容】 - [{}]",  new Date(), order.toString());
        if(order.getOrderStatus() == 0) {
            order.setOrderStatus(2);
            log.info("【該訂單未支付,取消訂單】" + order.toString());
        } else if(order.getOrderStatus() == 1) {
            log.info("【該訂單已完成支付】");
        } else if(order.getOrderStatus() == 2) {
            log.info("【該訂單已取消】");
        }
        log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
    }

}

4.發送者

package com.hmg.rabbitmq.config;

import com.hmg.rabbitmq.entity.Order;
import lombok.extern.log4j.Log4j2;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * @auther 吊炸天
 * @date 2019/12/18 21:48
 */
@Component
@Log4j2
public class DelaySender {

    @Autowired
    private AmqpTemplate amqpTemplate;

    public void sendDelay(Order order) {

        log.info("【訂單生成時間】" + new Date().toString() + "【1分鐘後檢查訂單是否已經支付】" + order.toString());
        this.amqpTemplate.convertAndSend(DelayRabbitConfig.ORDER_DELAY_EXCHANGE,
                DelayRabbitConfig.ORDER_DELAY_ROUTING_KEY, order, message -> {
                     /**
                      * 如果配置了 params.put("x-message-ttl", 5 * 1000);
                      * 那麼這一句也可以省略,
                      * 具體根據業務需要是聲明 Queue 的時候就指定好延遲時間還是在發送自己控制時間
                     */
                    message.getMessageProperties().setExpiration(1 * 1000 * 60 + "");
                    return message;
                });
    }
}

5.測試,訪問http://localhost:8080/sendDelay,查看日誌輸出

package com.hmg.rabbitmq.controller;

import com.hmg.rabbitmq.config.DelaySender;
import com.hmg.rabbitmq.entity.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @auther 吊炸天
 * @date 2019/12/18 21:56
 */
@RestController
public class TestController {

    @Autowired
    private DelaySender delaySender;



    @GetMapping("/sendDelay")
    public Object sendDelay() {
        Order order1 = new Order();
        order1.setOrderStatus(0);
        order1.setOrderId("13147747");
        order1.setOrderName("魅族16plus");

        Order order2 = new Order();
        order2.setOrderStatus(1);
        order2.setOrderId("68363685");
        order2.setOrderName("魅族16s");

        delaySender.sendDelay(order1);
        delaySender.sendDelay(order2);
        return "ok";
    }
}

6.測試效果
在這裏插入圖片描述

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