RabbitMQ的死信队列实现消息的延时消费

前言

在日常的开发中我们常常会遇到需要在一个事情完成之后的一段时间后做另一件事情(例如:下单成功后半小时未付款取消订单、用户注册成功五分钟后提醒用户绑定邮箱等),这种业务场景的特点是开始时间不确定,因此传统的定时任务不适合处理此类业务。

RabbitMQ 延时消息说明

RabbitMQ使用死信队列实现消息延时消费的原理就是消息发送到一个没有消费者的队列里,给队列或消息设置过期时间(消息和队列都设置了过期时间,按时间短的为准),等消息过期后自动发送到另一个队列里,消费者消费第二个队列里的消息就实现了消息的延时消费,延时的时间就是消息的过期时间。

RabbitMQ延时队列示意图

  • 延时路由key: 普通的路由key。
  • 死信路由key: 普通的路由key。
  • 延时交换机: 一个普通的交换机,延时消息发送到此交换机。
  • 延时队列: 根据延时路由key绑定到延时交换机,设置此队列的消息过期时间TTL、设置死信交换机、设置死信路由key。
  • 死信交换机: 一个普通交换机,叫它死信交换机的原因是它会接收延时队列里过期的消息(延时消息)。
  • 死信队列: 一个普通的队列,叫它死信队列的原因是它根据死信路由key绑定到死信交换机,最终延时队列里过期的消息(延时消息)会到此队列。延时消息的消费者从此队列消费消息。

Spring Boot + RabbitMQ 实现消息的延时消费

1、导入依赖

注: 导入 web 依赖是为了方便测试。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.qixi</groupId>
    <artifactId>qixi-mq-delay</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>qixi-mq-delay</name>
    <description>MQ Delay project for Spring Boot</description>

    <properties>
        <java.version>11</java.version>
    </properties>

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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </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>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

2、编写配置

server:
  port: 8080

spring:
  rabbitmq:
    host: 192.168.40.154
    port: 5672
    username: admin
    password: 123456
    virtual-host: /

3、定义交换机、队列、路由key

package com.qixi.mq.delay.common.constant;

/**
 * RabbitMQ常量
 *
 * @author ZhengNC
 * @date 2020/9/21 11:40
 */
public interface RabbitConstant {
    /**
     * 交换机
     */
    interface Exchanges{

        /**
         * 死信交换机
         */
        String deadExchange = "spring.boot.dead.exchange";

        /**
         * 延时交换机
         */
        String ttlExchange = "spring.boot.ttl.exchange";
    }

    /**
     * 队列
     */
    interface Queues{

        /**
         * 死信队列
         */
        String deadQueue = "spring.boot.dead.queue";

        /**
         * 延时队列
         */
        String ttlQueue = "spring.boot.ttl.queue";
    }

    /**
     * 路由key
     */
    interface RouterKey{

        /**
         * 死信路由key
         */
        String deadRouteKey = "dead.route.key";

        /**
         * 延时路由key
         */
        String ttlRouteKey = "ttl.route.key";
    }
}

4、配置 RabbitMQ 绑定关系

package com.qixi.mq.delay.config;

import com.qixi.mq.delay.common.constant.RabbitConstant;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

/**
 * RabbitMQ配置
 *
 * @author ZhengNC
 * @date 2020/9/14 10:40
 */
@Configuration
public class RabbitConfig {

    /**
     * 死信队列
     *
     * @return
     */
    @Bean("deadQueue")
    public Queue deadQueue(){
        return new Queue(RabbitConstant.Queues.deadQueue);
    }

    /**
     * 延时队列
     *
     * @return
     */
    @Bean("ttlQueue")
    public Queue ttlQueue1(){
        Map<String, Object> args = new HashMap<>(16);
        // 指定消息到期后发送到的死信交换机
        args.put("x-dead-letter-exchange", RabbitConstant.Exchanges.deadExchange);
        // 指定消息到期后发送到的路由键
        args.put("x-dead-letter-routing-key", RabbitConstant.RouterKey.deadRouteKey);
        // 声明队列的过期时间(TTL)(单位:毫秒)
        args.put("x-message-ttl", 10000);
        return QueueBuilder
                .durable(RabbitConstant.Queues.ttlQueue)
                .withArguments(args)
                .build();
    }

    /**
     * 死信队列交换机
     *
     * @return
     */
    @Bean("deadExchange")
    public DirectExchange deadExchange(){
        return new DirectExchange(RabbitConstant.Exchanges.deadExchange);
    }

    /**
     * 延时队列交换机
     *
     * @return
     */
    @Bean("ttlExchange")
    public DirectExchange ttlExchange(){
        return new DirectExchange(RabbitConstant.Exchanges.ttlExchange);
    }

    /**
     * 绑定延时队列和延时交换机
     *
     * @param ttlQueue
     * @param ttlExchange
     * @return
     */
    @Bean
    public Binding ttlQueue_ttlExchange(
            @Qualifier("ttlQueue") Queue ttlQueue,
            @Qualifier("ttlExchange") DirectExchange ttlExchange){
        return BindingBuilder.bind(ttlQueue)
                .to(ttlExchange)
                .with(RabbitConstant.RouterKey.ttlRouteKey);
    }

    /**
     * 绑定死信队列和死信交换机
     *
     * @param deadQueue
     * @param deadExchange
     * @return
     */
    @Bean
    public Binding deadQueue_deadExchange(
            @Qualifier("deadQueue") Queue deadQueue,
            @Qualifier("deadExchange") DirectExchange deadExchange){
        return BindingBuilder.bind(deadQueue)
                .to(deadExchange)
                .with(RabbitConstant.RouterKey.deadRouteKey);
    }
}

5、延时消息生产者

package com.qixi.mq.delay.producer;

import com.qixi.mq.delay.common.constant.RabbitConstant;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * 延时消息生产者
 *
 * @author ZhengNC
 * @date 2020/9/21 14:15
 */
@Service
public class TTLProducer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 发送一条延时消息
     *
     * @param message
     */
    public void sendTTLMessage(String message){
        rabbitTemplate.convertAndSend(
                RabbitConstant.Exchanges.ttlExchange,
                RabbitConstant.RouterKey.ttlRouteKey,
                message);
    }
}

6、延时消息的消费者

package com.qixi.mq.delay.consumer;

import com.qixi.mq.delay.common.constant.RabbitConstant;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

/**
 * 延时消息的消费者
 *
 * @author ZhengNC
 * @date 2020/9/21 14:08
 */
@Component
public class TTLConsumer {

    /**
     * 消费延时消息
     *
     * @param message
     */
    @RabbitListener(queues = RabbitConstant.Queues.deadQueue)
    public void ttlConsumer(String message){
        System.out.println("消费了一条消息,消费时间:"
                + DateTimeFormatter.ofPattern("HH:mm:ss")
                .format(LocalTime.now()));
        System.out.println(message);
    }
}

7、编写接口测试发送消息

统一接口响应格式:

package com.qixi.mq.delay.common.dto;

import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * 统一接口响应格式
 *
 * @author ZhengNC
 * @date 2020/9/15 14:24
 */
@Getter
@Setter
public class ResponseEntity<T> {

    private String code = "200";

    private String msg = "success";

    private String timestamp = DateTimeFormatter
            .ofPattern("yyyy-MM-dd HH:mm:ss")
            .format(LocalDateTime.now());

    private T data;

    public ResponseEntity(){}

    public ResponseEntity(T data) {
        this.data = data;
    }

    public ResponseEntity(String code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public static ResponseEntity success(){
        return new ResponseEntity();
    }

    public static ResponseEntity fail(){
        return new ResponseEntity("500", "fail", null);
    }
}

http接口:

package com.qixi.mq.delay.controller;

import com.qixi.mq.delay.common.dto.ResponseEntity;
import com.qixi.mq.delay.producer.TTLProducer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

/**
 * @author ZhengNC
 * @date 2020/9/21 14:27
 */
@RestController
@RequestMapping("ttl")
public class TTLProducerController {

    @Autowired
    private TTLProducer producer;

    /**
     * 发送延时消息
     *
     * @return
     */
    @GetMapping("send")
    public ResponseEntity autoSend(){
        DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss");
        StringBuilder message = new StringBuilder("这是一条延时消息,消息的发送时间为:");
        message.append(timeFormatter.format(LocalTime.now()));
        producer.sendTTLMessage(message.toString());
        return ResponseEntity.success();
    }
}

8、测试结果

消费了一条消息,消费时间:14:48:38
这是一条延时消息,消息的发送时间为:14:48:28

©️2020 CSDN 皮肤主题: 游动-白 设计师:上身试试 返回首页