SpringBoot整合RabbitMQ实现延迟队列

首先引入依赖,由于springboot starter指定了rabbitMQ的版本,所以无需在引入依赖的时候指定版本


        
            org.springframework.boot
            spring-boot-starter-amqp
        

配置文件

# 应用名称
spring:
  application:
    name: rabbitmq
  rabbitmq:
    host: 192.168.227.4
    port: 5672
    username: admin
    password: admin
    publisher-confirm-type: correlated #发布确认
    publisher-returns: true #路由失败 消息回退
# 应用服务 WEB 访问端口
server:
  port: 8080

1、ttl死信队列

通过设置队列的ttl过期时间,当消息在队列中超过规定的ttl时间时未被消费时,便会抛到绑定的死信队列中去消费

1.1、配置类

用于初始化交换机、队列和路由

package com.example.rabbitmq.config;

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;

/**
 * @program: rabbitmq
 * @description: ttl延迟队列配置类 @Bean可以设置组件的名字 没有设置时以方法名为组件名字
 * @author: LiuZhuzheng
 * @create: 2021-12-03 16:07
 **/
@Configuration
public class TtlQueueConfig {
    /** 普通交换机*/
    public static final String NORMAL_EXCHANGE = "normal_exchange";
    /** 死信交换机*/
    public static final String DEAD_EXCHANGE = "dead_exchange";
    /** 普通队列a*/
    public static final String NORMAL_QUEUE_A = "normal_queue_a";
    /** 普通队列b*/
    public static final String NORMAL_QUEUE_B = "normal_queue_b";
    /** 普通队列c*/
    public static final String NORMAL_QUEUE_C = "normal_queue_c";
    /** 死信队列*/
    public static final String DEAD_QUEUE = "dead_queue";
    /** 普通队列a路由key*/
    public static final String NORMAL_ROUTING_KEY_A = "normal_routing_key_a";
    /** 普通队列b路由key*/
    public static final String NORMAL_ROUTING_KEY_B = "normal_routing_key_b";
    /** 普通队列c路由key*/
    public static final String NORMAL_ROUTING_KEY_C = "normal_routing_key_c";
    /** 死信队列路由key*/
    public static final String DEAD_ROUTING_KEY = "dead_routing_key";

    /**
     * 声明普通交换机
     * 使用直接交换机 发布订阅方式
     * 默认持久化 消费者断开连接时不自动删除
     * */
    @Bean
    public DirectExchange normalExchange(){
        return new DirectExchange(NORMAL_EXCHANGE);
    }

    /**
     * 声明死信交换机
     * */
    @Bean
    public DirectExchange deadExchange(){
        return new DirectExchange(DEAD_EXCHANGE);
    }

    /**
     * 声明普通队列a
     * 设置ttl时间为10秒
     * */
    @Bean
    public Queue queueA(){
        // Map params = new HashMap<>();
        // //声明当前队列绑定的死信交换机
        // params.put("x-dead-letter-exchange",DEAD_EXCHANGE);
        // //声明当前队列的死信路由key
        // params.put("x-dead-letter-routing-key",DEAD_ROUTING_KEY);
        // //声明队列ttl
        // params.put("x-message-ttl",10000);
        // return QueueBuilder.durable(NORMAL_QUEUE_A).withArguments(params).build();
        return QueueBuilder.durable(NORMAL_QUEUE_A)
                .deadLetterExchange(DEAD_EXCHANGE)
                .deadLetterRoutingKey(DEAD_ROUTING_KEY)
                .ttl(10000)
                .build();
    }

    /**
     * 声明普通队列b
     * 设置ttl时间为40秒
     * */
    @Bean
    public Queue queueB(){
        return QueueBuilder.durable(NORMAL_QUEUE_B)
                .deadLetterExchange(DEAD_EXCHANGE)
                .deadLetterRoutingKey(DEAD_ROUTING_KEY)
                .ttl(40000)
                .build();
    }

    /**
     * 声明普通队列c
     * */
    @Bean
    public Queue queueC(){
        return QueueBuilder.durable(NORMAL_QUEUE_C)
                .deadLetterExchange(DEAD_EXCHANGE)
                .deadLetterRoutingKey(DEAD_ROUTING_KEY)
                .build();
    }

    /**
     * 声明死信队列
     * */
    @Bean
    public Queue queueD(){
        return QueueBuilder.durable(DEAD_QUEUE).build();
    }

    /**
     * 声明普通队列a与普通交换机绑定 设置路由key
     * 参数因为已经注册在容器中 会自动从容器中取 但是名字必须一样
     * */
    @Bean
    public Binding queueBindingA(Queue queueA, DirectExchange normalExchange){
        return BindingBuilder.bind(queueA).to(normalExchange).with(NORMAL_ROUTING_KEY_A);
    }

    /**
     * 声明普通队列b与普通交换机绑定 设置路由key
     * */
    @Bean
    public Binding queueBindingB(Queue queueB, DirectExchange normalExchange){
        return BindingBuilder.bind(queueB).to(normalExchange).with(NORMAL_ROUTING_KEY_B);
    }

    /**
     * 声明普通队列c与普通交换机绑定 设置路由key
     * */
    @Bean
    public Binding queueBindingC(Queue queueC, DirectExchange normalExchange){
        return BindingBuilder.bind(queueC).to(normalExchange).with(NORMAL_ROUTING_KEY_C);
    }

    /**
     * 声明死信队列与死信交换机绑定
     * */
    @Bean
    public Binding queueBindingD(Queue queueD, DirectExchange deadExchange){
        return BindingBuilder.bind(queueD).to(deadExchange).with(DEAD_ROUTING_KEY);
    }
}

1.2、controller控制器

接收页面请求发送消息

@Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 普通发送消息 基于死信队列
     * */
    @GetMapping("/sendMsg/{msg}")
    public void sendMsg(@PathVariable("msg") String msg){
        log.info("当前时间:{},发送一条消息给两个队列:{}",System.currentTimeMillis(),msg);
        rabbitTemplate.convertAndSend("normal_exchange","normal_routing_key_a","延时10秒的队列a" + msg);
        rabbitTemplate.convertAndSend("normal_exchange","normal_routing_key_b","延时40秒的队列a" + msg);
    }

1.3、死信队列消费者

通过注解监听死信队列来消费消息

package com.example.rabbitmq.listener;

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

import java.util.Objects;

/**
 * @program: rabbitmq
 * @description: 死信队列消费者 利用监听器
 * @author: LiuZhuzheng
 * @create: 2021-12-03 16:56
 **/
@Component
@Slf4j
public class DeadLetterQueueConsumer {
    /**
     * 接收消息
     * */
    @RabbitListener(queues = "dead_queue")
    public void receiveDeadMessage(Message message, Channel channel) throws Exception{
        String msg = new String(message.getBody());
        log.info("当前时间:{},收到死信队列消息:{}",System.currentTimeMillis(),msg);
    }

}

显而易见,该方法存在一个缺陷,就是一个ttl时间对应一个队列,每增加一个ttl时间的需求就需要增加一个队列

2、ttl死信队列优化

针对上述问题,创建一个新队列c(上述代码已有),在初始化队列时不设置ttl时间,而是设置消息的ttl过期时间,其控制器代码如下

    /**
     * 设置过期时间的发送消息 基于死信队列
     * */
    @GetMapping("/sendMsg/{msg}/{ttl}")
    public void sendMsgWithTtl(@PathVariable("msg") String msg, @PathVariable("ttl")String ttl){
        log.info("当前时间:{},{}毫秒后发送消息:{}", Instant.now(),ttl,msg);
        rabbitTemplate.convertAndSend("normal_exchange", "normal_routing_key_b", msg, message -> {
            message.getMessageProperties().setExpiration(ttl);
            return message;
        });
    }

这样每条消息的过期时间都可以自己设定

但是当测试时,先发送一条延时20秒的消息,在发送一条延时2秒的消息,发现后者并不会优先得到执行,而是等待前者执行完毕后立即执行

3、RabbitMQ插件实现延时队列

3.1、安装插件

访问官网:https://www.rabbitmq.com/community-plugins.html

往下翻找到rabbitmq_delayed_message_exchange并下载

进入 RabbitMQ 的安装目录下的 plgins 目录,执行下面命令让该插件生效,然后重启 RabbitMQ
rabbitmq-plugins enable rabbitmq_delayed_message_exchange

3.2、配置类

声明延时队列、交换机和路由key

package com.example.rabbitmq.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.CustomExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

/**
 * @program: rabbitmq
 * @description: 利用插件实现延迟队列配置类
 * @author: LiuZhuzheng
 * @create: 2021-12-15 20:11
 **/
@Configuration
public class DelayedQueueConfig {
    public static final String DELAYED_QUEUE_NAME = "delayed_queue";

    public static final String DELAYED_EXCHANGE_NAME = "delayed_exchange";

    public static final String DELAYED_ROUTING_KEY = "delayed_key";

    /**
     * 声明自定义交换机
     * */
    @Bean
    public CustomExchange delayedExchange(){
        Map argument = new HashMap<>(4);
        argument.put("x-delayed-type", "direct");

        return new CustomExchange(DELAYED_EXCHANGE_NAME,"x-delayed-message",true,false,argument);
    }

    /**
     * 声明队列
     * */
    @Bean
    public Queue delayedQueue(){
        return new Queue(DELAYED_QUEUE_NAME);
    }

    /**
     * 绑定交换机和队列
     * */
    @Bean
    public Binding delayedQueueBinding(Queue delayedQueue, CustomExchange delayedExchange){
        return BindingBuilder.bind(delayedQueue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
    }
}

3.3、controller控制器

    /**
     * 基于插件发送延迟消息
     * */
    @GetMapping("/sendDelayedMsg/{msg}/{delayed}")
    public void sendDelayedMsg(@PathVariable("msg")String msg, @PathVariable("delayed")Integer delayed){
        log.info("当前时间:{},{}毫秒后给延迟队列发送消息:{}", Instant.now(),delayed,msg);
        rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME, DelayedQueueConfig.DELAYED_ROUTING_KEY, msg, message -> {
            message.getMessageProperties().setDelay(delayed);
            return message;
        });
    }

3.4、延时队列消费者

和上述类似

package com.example.rabbitmq.listener;

import com.example.rabbitmq.config.DelayedQueueConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.time.Instant;
import java.util.Arrays;

/**
 * @program: rabbitmq
 * @description: 延迟队列消费者 基于插件
 * @author: LiuZhuzheng
 * @create: 2021-12-15 20:30
 **/
@Component
@Slf4j
public class DelayQueueConsumer {
    /**
     * 接收消息
     * */
    @RabbitListener(queues = DelayedQueueConfig.DELAYED_QUEUE_NAME)
    public void receiveDelayedMessage(Message message){
        String s = new String(message.getBody());
        log.info("当前时间:{},收到延迟队列消息:{}", Instant.now(),s);
    }
}

这样就解决了延时时间短的消息不能优先消费的问题

在生产环境中,可能RabbitMQ会挂掉或者重启,就有可能导致生产者消息投递失败,从而导致消息丢失,需要手动处理和恢复。

4、发布确认

4.1、配置文件

在配置文件中需要添加publisher-confirm-type=correlated,如文章开头所示

4.2、配置类

声明一些交换机、队列并进行绑定

普通交换机通过alternate方法绑定了备份交换机,因此在普通交换机中的消息因为消息生产者设置的路由key错误等导致不能被消费时,不会发生4.4回调中消息退回给生产者,而是会通过备份交换机转发给备份队列和报警队列进行消费。

package com.example.rabbitmq.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @program: rabbitmq
 * @description: 发布确认高级配置
 * @author: LiuZhuzheng
 * @create: 2021-12-18 09:27
 **/
@Configuration
public class ConfirmQueueConfig {
    public static final String CONFIRM_EXCHANGE_NAME = "confirm_exchange";

    public static final String CONFIRM_QUEUE_NAME = "confirm_queue";

    public static final String CONFIRM_ROUTING_KEY = "confirm_key";

    /**
     * 备份交换机 队列
     * */
    public static final String BACKUP_EXCHANGE_NAME = "backup_exchange";

    public static final String BACKUP_QUEUE_NAME = "backup_queue";
    /**
     * 报警队列
     * */
    public static final String WARNING_QUEUE_NAME = "warning_queue";

    /**
     * 声明交换机
     * */
    @Bean
    public DirectExchange confirmExchange(){
        return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME).alternate(BACKUP_EXCHANGE_NAME).build();
    }

    /**
     * 声明队列
     * */
    @Bean
    public Queue confirmQueue(){
        return new Queue(CONFIRM_QUEUE_NAME);
    }

    /**
     * 交换机与队列绑定
     * */
    @Bean
    public Binding confirmQueueBinding(DirectExchange confirmExchange, Queue confirmQueue){
        return BindingBuilder.bind(confirmQueue).to(confirmExchange).with(CONFIRM_ROUTING_KEY);
    }

    /**
     * 备份交换机
     * */
    @Bean
    public FanoutExchange backupExchange(){
        return new FanoutExchange(BACKUP_EXCHANGE_NAME);
    }

    /**
     * 备份队列
     * */
    @Bean
    public Queue backupQueue(){
        return new Queue(BACKUP_QUEUE_NAME);
    }

    /**
     * 报警队列
     * */
    @Bean
    public Queue warningQueue(){
        return new Queue(WARNING_QUEUE_NAME);
    }

    /**
     * 备份绑定
     * */
    @Bean
    public Binding backupBind(FanoutExchange backupExchange, Queue backupQueue){
        return BindingBuilder.bind(backupQueue).to(backupExchange);
    }

    /**
     * 报警绑定
     * */
    @Bean
    public Binding warningBind(FanoutExchange backupExchange, Queue warningQueue){
        return BindingBuilder.bind(warningQueue).to(backupExchange);
    }
}

4.3、消息生产者(Controller)

package com.example.rabbitmq.controller;

import com.example.rabbitmq.config.ConfirmQueueConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.nio.charset.StandardCharsets;

/**
 * @program: rabbitmq
 * @description: 发布确认高级控制器
 * @author: LiuZhuzheng
 * @create: 2021-12-18 09:39
 **/
@Slf4j
@RestController
@RequestMapping("/confirm")
public class ConfirmController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 发消息 测试发布确认高级
     * */
    @GetMapping("/sendMessage/{message}")
    public void sendMessage(@PathVariable("message") String message){
        // 设置回调对象 发布确认
        if (StringUtils.isEmpty(message)){
            log.info("消息为空");
            return;
        }
        Message msg = new Message(message.getBytes(StandardCharsets.UTF_8),new MessageProperties());
        CorrelationData correlationData = new CorrelationData("1");
        correlationData.setReturnedMessage(msg);
        rabbitTemplate.convertAndSend(ConfirmQueueConfig.CONFIRM_EXCHANGE_NAME, ConfirmQueueConfig.CONFIRM_ROUTING_KEY + "1", message, correlationData);
        log.info("发送消息为:{}",message);
    }
}

4.4、发布确认回调类(交换机)

当消息生产者发送消息到达交换机时,就会触发该回调类

package com.example.rabbitmq.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.io.IOException;

/**
 * @program: rabbitmq
 * @description: 发布确认回调类 交换机
 * @author: LiuZhuzheng
 * @create: 2021-12-18 09:54
 **/
@Slf4j
@Component
public class MyConfirmCallBack implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 将当前接口注入 Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct(注释的方法)
     * 1 将当前类实例化
     * 2 将rabbitTemplate自动注入
     * 3 执行下面方法
     * */
    @PostConstruct
    public void init(){
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnCallback(this);
    }

    /**
     * correlationData保存回调消息的ID及相关信息
     * 第二个参数收到消息为true 没收到为false
     * 第三个参数收到消息为null 没收到为失败原因
     * */
    @Override
    public void confirm(CorrelationData correlationData, boolean b, String s) {
        String id = correlationData != null ? correlationData.getId() : "";
        String message = correlationData != null ? new String(correlationData.getReturnedMessage().getBody()) : "";
        if (b){
            log.info("交换机收到了消息,id为:{},消息为:{}", id, message);
        }else {
            log.info("交换机未收到id为{}的消息:{},原因是:{}",id, message, s);
        }
    }

    /**
     * 消息传递过程中不可达目的地将消息返回给生产者
     * */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        log.info("消息:{}被服务器退回,退回代码:{}, 退回原因:{}, 交换机是:{}, 路由 key:{}",
                new String(message.getBody()), replyCode, replyText, exchange, routingKey);
    }
}

4.5、消息消费者

package com.example.rabbitmq.listener;

import com.example.rabbitmq.config.ConfirmQueueConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * @program: rabbitmq
 * @description: 发布确认高级消费者
 * @author: LiuZhuzheng
 * @create: 2021-12-18 09:44
 **/
@Slf4j
@Component
public class ConfirmQueueConsumer {

    @RabbitListener(queues = ConfirmQueueConfig.CONFIRM_QUEUE_NAME)
    public void receiveMessage(Message message){
        String msg = new String(message.getBody());
        log.info("接收到confirm队列消息:{}",msg);
    }
}

4.6、备份和报警消费者

package com.example.rabbitmq.listener;

import com.example.rabbitmq.config.ConfirmQueueConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * @program: rabbitmq
 * @description: 报警消费者
 * @author: LiuZhuzheng
 * @create: 2021-12-18 11:14
 **/
@Slf4j
@Component
public class WarningQueueConsumer {

    @RabbitListener(queues = ConfirmQueueConfig.WARNING_QUEUE_NAME)
    public void receiveMsg(Message message){
        String msg = new String(message.getBody());
        log.info("报警发现不可路由消息:{}",msg);
    }

    @RabbitListener(queues = ConfirmQueueConfig.BACKUP_QUEUE_NAME)
    public void receiveMessage(Message message){
        String msg = new String(message.getBody());
        log.info("发现不可路由消息:{}",msg);
    }
}

你可能感兴趣的:(rabbitmq,spring,boot,java)