RabbitMQ使用延迟队列(通俗易懂)

场景
延迟消息是指的消息发送出去后并不想立即就被消费,而是需要等(指定的)一段时间后才触发消费。
  • 订单创建成功后,需要30分钟内支付成功。就可以用延迟队列,订单创建成功后发送一个延迟消息,这条消息30分钟后才能被消费,消费的时候去查询订单状态是否是已支付。
  • 公司预约会议,22点有会议,21:45(提前15分钟)就通知参会人员最好准备,快开会了。
实现方式
延迟队列在AMQP协议和RabbitMQ中都没有相关的规定和实现。
  • 可以借助“死信队列”来变相的实现(消息到期后加入死信队列,然后定义一个消费者消费死信队列的消息即可。PS:代码中有完整实现);
  • 可以使用rabbitmq_delayed_message_exchange插件实现。

下面我们用插件的方式来实现延迟队列

延迟消息消费流程:
  1. 生产者将消息(msg)和路由键(routekey)发送指定的延迟交换机(exchange)上
  2. 延迟交换机(exchange)存储消息等待消息到期根据路由键(routekey)找到绑定自己的队列(queue)并把消息给它(消息到期后才会到达队列
  3. 队列(queue)再把消息发送给监听它的消费者(customer)

文末有代码git地址(包含路由(Direct)模式、Work模式、主题(Topic)模式、发布订阅/广播(Fanout)模式、TTL、死信队列、延迟队列)

安装延迟队列插件

安装RabbitMQ教程:Mac安装RabbitMQ

1、下载插件
下载地址:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases
2、将需要安装的插件拷贝到插件位置 
/usr/local/Cellar/rabbitmq/3.9.7/plugins
然后查看插件列表
rabbitmq-plugins list
结果如下
[E*]、[e*]都是已安装的,其他都是未安装的
3、启用插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
再次查看插件列表
4、重启RabbitMQ
# 关闭RabbitMQ
rabbitmqctl stop
# 后台启动RabbitMQ
rabbitmq-server -detached

SpringBoot整合RabbitMQ使用延迟队列

1、添加依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--amqp依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.12</version>
        </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>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

2、添加配置文件application.yml

server:
  port: 8899
spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    publisher-confirm-type: correlated  # 消息发送到交换机确认机制,是否确认回调。correlated&simple相当于publisher-confirms: true,none相当于publisher-confirms: false
    publisher-returns: true   # 消息发送到队列确认机制,是否确认回调(消息发送失败返回队列中)
    virtual-host: /    #连接到rabbitMQ的vhost
    # 如果使用自定义监听器,则下面的配置可以注释掉(也可以不用管)
    listener:
      simple:
        acknowledge-mode: manual  #manual:采用手动应答;none:不确认;auto:自动确认,若采用手动确认,可以自定义监听器,不使用@RabbitListener注解来消费消息
        prefetch: 1 #限制每次发送一条数据。
        concurrency: 1 #指定最小消费者数量
        max-concurrency: 1 #指定最大消费者数量
        default-requeue-rejected: false  #重试超过最大次数后是否拒绝
        retry:
          enabled: true #是否开启消费者重试(为false时关闭消费者重试,意思不是“不重试”,而是消费端代码异常会一直重复收到消息直到ack确认或者一直到超时)
          max-attempts: 5 #最大重试次数
          initial-interval: 500 #重试间隔时间(单位毫秒)第一次和第二次尝试发布或传递消息之间的间隔
          max-interval: 1000 #重试最大时间间隔(单位毫秒)
          multiplier: 5 #应用于上一重试间隔的乘数
logging:
  level:
    com:
      qjc:
        mq: debug

3、定义常量

public class RabbitMQConstant {

    /**
     * 延迟交换器 
     */
    public static final String EXCHANGE_DELAY = "delay_exchange";
    
    /**
     * 延迟队列 
     */
    public static final String QUEUE_DELAY = "delay.queue";
    
    /**
     * 延迟队列路由key 
     */
    public static final String ROUTING_KEY_DELAY = "routing.key.delay";
    
}

4、定义交换机

package com.qjc.mq.config;

import com.qjc.mq.constant.RabbitMQConstant;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Description:
 * @Author: qjc
 * @Date: 2021/12/6
 */
@Configuration
public class ExchangeConfig {

    /**
     * 交换机说明:
     * durable="true" rabbitmq重启的时候不需要创建新的交换机
     * auto-delete 表示交换机没有在使用时将被自动删除 默认是false
     * direct交换器相对来说比较简单,匹配规则为:如果路由键匹配,消息就被投送到相关的队列
     * topic交换器你采用模糊匹配路由键的原则进行转发消息到队列中
     * fanout交换器中没有路由键的概念,他会把消息发送到所有绑定在此交换器上面的队列中。
     */
    @Bean(name = RabbitMQConstant.EXCHANGE_DELAY)
    public DirectExchange delayExchange() {
        DirectExchange directExchange = new DirectExchange(RabbitMQConstant.EXCHANGE_DELAY, true, false);
        directExchange.setDelayed(true);
        return directExchange;
//        // 使用自定义交换器
//        Map<String, Object> props = new HashMap<>(2);
//        props.put("x-delayed-type", ExchangeTypes.FANOUT);
//        return new CustomExchange(RabbitMQConstant.EXCHANGE_DELAY, "x-delayed-message", true, false, props);
    }

}

5、定义队列

package com.qjc.mq.config;

import com.qjc.mq.constant.RabbitMQConstant;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Description:
 * @Author: qjc
 * @Date: 2021/12/6
 */
@Configuration
public class QueueConfig {

    /**
     * durable="true" 持久化 rabbitmq重启的时候不需要创建新的队列
     * exclusive 表示该消息队列是否只在当前connection生效,默认是false
     * auto-delete 表示消息队列没有在使用时将被自动删除 默认是false
     */
    @Bean(name = RabbitMQConstant.QUEUE_DELAY)
    public Queue delayQueue() {
        return new Queue(RabbitMQConstant.QUEUE_DELAY, true, false, false);
    }

}

6、绑定

package com.qjc.mq.config;

import com.qjc.mq.callbackconfig.MsgSendConfirmCallback;
import com.qjc.mq.callbackconfig.MsgSendReturnCallback;
import com.qjc.mq.constant.RabbitMQConstant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;

/**
 * @Description:
 * @Author: qjc
 * @Date: 2021/12/6
 */
@Configuration
@Slf4j
public class RabbitMqConfig {

    @Resource
    private QueueConfig queueConfig;
    @Resource
    private ExchangeConfig exchangeConfig;
    @Resource
    private ConnectionFactory connectionFactory;


    @Bean
    public Binding bindingDelay() {
        // 使用自定义交换器时后面需要添加.noargs()
//        return BindingBuilder.bind(queueConfig.delayQueue()).to(exchangeConfig.delayExchange()).with(RabbitMQConstant.ROUTING_KEY_DELAY).noargs();
        return BindingBuilder.bind(queueConfig.delayQueue()).to(exchangeConfig.delayExchange()).with(RabbitMQConstant.ROUTING_KEY_DELAY);
    }

    /** ======================== 定制一些处理策略 =============================*/

    /**
     * 定制化amqp模版
     * <p>
     * Rabbit MQ的消息确认有两种。
     * <p>
     * 一种是消息发送确认:这种是用来确认生产者将消息发送给交换机,交换机传递给队列过程中,消息是否成功投递。
     * 发送确认分两步:一是确认是否到达交换机,二是确认是否到达队列
     * <p>
     * 第二种是消费接收确认:这种是确认消费者是否成功消费了队列中的消息。
     */
    @Bean
    public RabbitTemplate rabbitTemplate() {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        // 消息发送失败返回到队列中, yml需要配置 publisher-returns: true
        rabbitTemplate.setMandatory(true);

        /**
         * 使用该功能需要开启消息确认,yml需要配置 publisher-confirms: true
         * 通过实现ConfirmCallBack接口,用于实现消息发送到交换机Exchange后接收ack回调
         * correlationData  消息唯一标志
         * ack              确认结果
         * cause            失败原因
         */
        rabbitTemplate.setConfirmCallback(new MsgSendConfirmCallback());
        /**
         * 使用该功能需要开启消息返回确认,yml需要配置 publisher-returns: true
         * 通过实现ReturnCallback接口,如果消息从交换机发送到对应队列失败时触发
         * message    消息主体 message
         * replyCode  消息主体 message
         * replyText  描述
         * exchange   消息使用的交换机
         * routingKey 消息使用的路由键
         */
        rabbitTemplate.setReturnCallback(new MsgSendReturnCallback());


        return rabbitTemplate;
    }

}

7、消息发送成功的处理策略

package com.qjc.mq.callbackconfig;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

/**
 * @Description: 消息发送到交换机确认机制
 * @Author: qjc
 * @Date: 2021/12/7
 */
@Component
@Slf4j
public class MsgSendConfirmCallback implements RabbitTemplate.ConfirmCallback {

    /**
     * 使用该功能需要开启消息确认,yml需要配置 publisher-confirms: true
     * correlationData  消息唯一标志
     * ack              确认结果
     * cause            失败原因
     * <p>
     * PS:通过实现ConfirmCallBack接口,用于实现消息发送到交换机Exchange后接收ack回调
     * </p>
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            log.debug("消息发送到exchange成功,id: {}", correlationData.getId());
        } else {
            log.debug("消息{}发送到exchange失败,原因: {}", correlationData.getId(), cause);
        }
    }
}
package com.qjc.mq.callbackconfig;

import com.qjc.mq.constant.RabbitMQConstant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

/**
 * @Description:
 * @Author: qjc
 * @Date: 2021/12/7
 */
@Component
@Slf4j
public class MsgSendReturnCallback implements RabbitTemplate.ReturnCallback {

    /**
     * 使用该功能需要开启消息返回确认,yml需要配置 publisher-returns: true
     * message    消息主体 message
     * replyCode  消息主体 message
     * replyText  描述
     * exchange   消息使用的交换机
     * routingKey 消息使用的路由键
     * <p>
     * PS:通过实现ReturnCallback接口,如果消息从交换机发送到对应队列失败时触发
     * </p>
     */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        if (exchange.equals(RabbitMQConstant.EXCHANGE_DELAY)) {
            // 如果配置了发送回调ReturnCallback,rabbitmq_delayed_message_exchange插件会回调该方法,因为发送方确实没有投递到队列上,只是在交换器上暂存,等过期/时间到了才会发往队列。
            // 所以如果是延迟队列的交换器,则直接放过,并不是bug
            return;
        }
        String correlationId = message.getMessageProperties().getCorrelationId();
        log.debug("消息:{} 发送失败, 应答码:{} 原因:{} 交换机: {}  路由键: {}", correlationId, replyCode, replyText, exchange, routingKey);
    }
}

8、定义消息发送者

package com.qjc.mq.mqsender.delay;

import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import com.qjc.mq.constant.RabbitMQConstant;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Date;
import java.util.UUID;

/**
 * @ClassName: DelaySender
 * @Description:
 * @Author: qjc
 * @Date: 2021/12/9 11:42 上午
 */
@Component
public class DelaySender {

    @Resource
    RabbitTemplate rabbitTemplate;

    public String send(Integer seconds) {
        DateTime dateTime = DateUtil.offsetSecond(new Date(), seconds);
        String msg = "通知时间(" + DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss") + "),通知内容(" + DateUtil.formatDateTime(dateTime) + "召开会议)";
        Message message = MessageBuilder.withBody(msg.getBytes()).build();
        // RabbitMQ只会检查队列头部的消息是否过期,如果过期就放到死信队列
        // 假如第一个过期时间很长,10s,第二个消息3s,则系统先看第一个消息,等 到第一个消息过期,放到DLX
        // 此时才会检查第二个消息,但实际上此时第二个消息早已经过期了,但是并没 有先于第一个消息放到DLX。
        // 插件rabbitmq_delayed_message_exchange帮我们搞定这个。
        MessageProperties messageProperties = message.getMessageProperties();
        // 设置到期时间,也就是提前10s提醒
        messageProperties.setDelay((seconds - 10) * 1000);
        rabbitTemplate.convertAndSend(RabbitMQConstant.EXCHANGE_DELAY, RabbitMQConstant.ROUTING_KEY_DELAY, message, new CorrelationData(UUID.randomUUID().toString().replaceAll("-", "")));
        return seconds + "秒后召开会议,已经定好闹钟了,到时提前告诉大家";
    }

}

9、定义消息消费者

package com.qjc.mq.mqreceiver.delay;

import com.qjc.mq.constant.RabbitMQConstant;
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.text.SimpleDateFormat;
import java.util.Date;

/**
 * @ClassName: DelayReceiver
 * @Description:
 * @Author: qjc
 * @Date: 2021/12/9 11:39 上午
 */
@Component
@Slf4j
public class DelayReceiver {

    @RabbitListener(queues = RabbitMQConstant.QUEUE_DELAY)
    public void process(Message message, Channel channel) throws Exception {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.err.println("接到通知【" + new String(message.getBody(), "utf-8") + "】,接收时间【" + simpleDateFormat.format(new Date()) + "】");
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        channel.basicAck(deliveryTag, false);
    }

}

10、创建controller

package com.qjc.mq.controller;

import com.qjc.mq.mqsender.delay.DelaySender;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;


/**
 * @Description:
 * @Author: qjc
 * @Date: 2021/12/6
 */
@RestController
@CrossOrigin(origins = "*", methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.DELETE, RequestMethod.PUT})
public class MqSenderController {

    /**
     * rabbitMQ 的核心就是  交换器,路由键,队列;
     * <p>
     * 消息来了之后,会发送到交换器,交换器将根据路由键将消息发送到相对应的队列里面去!
     * <p>
     * 消息不用关系到达了那个队列.只需要带着自己的路由键到了交换器就可以了,交换器会帮你把消息发送到指定的队列里面去!
     */
    @Resource
    DelaySender delaySender;


    /**
     * 测试延迟队列: http://localhost:8899/delay/20
     *
     * @param seconds 多少秒后开会
     */
    @RequestMapping(value = "/delay/{seconds}", method = {RequestMethod.GET})
    public String delaySender(@PathVariable Integer seconds) {
        return delaySender.send(seconds);
    }


}

结果如下

 

 完整代码地址

(包含路由(Direct)模式、Work模式、主题(Topic)模式、发布订阅/广播(Fanout)模式、TTL、死信队列、延迟队列):

https://gitee.com/xiaorenwu_dashije/rabbitmq_demo.git

 

posted @ 2021-12-09 16:04  劈天造陆  阅读(1408)  评论(1编辑  收藏  举报