RabbitMQ(5)SpringBoot+RabbitMQ用死信队列和插件形式实现延迟队列

RabbitMQ(5)SpringBoot+RabbitMQ用死信队列和插件形式实现延迟队列_第1张图片
RabbitMQ

死信队列:用来保存处理失败或者过期的消息,确保消息不被丢失以便排查问题!

延迟队列:顾名思义就是消息在队列中存在一定时间后再被消费。比如下单后半小时没有支付的订单自动取消,比如预约某项功能时提前15分钟提醒,比如希望某一个功能在多长时间后执行等都可以使用延迟队列。

1. 死信队列之延迟队列
  • RabbitMQ本身是没有延迟队列功能的,但是可以通过死信队列的TTL和DLX模拟延迟队列功能。
  • 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. 队列达到最大的长度
RabbitMQ(5)SpringBoot+RabbitMQ用死信队列和插件形式实现延迟队列_第2张图片
死信队列
  • 做延迟队列需要创建一个没有消费者的队列,用了存储消息。然后创建一个真正的消费队列,用来做具体的业务逻辑。当带有TTL的消息到达绑定死信交换器的队列,因为没有消费者所以会一直等到消息过期,然后消息被投递到死信队列也就是真正的消费队列。
  • 新建配置类MQDelayConfig.java,创建支付交换器、支付队列绑定死信队列、他们的绑定关系。无消费者,暂时不知道怎么用注解创建。
  • 设置x-dead-letter-exchange、x-dead-letter-routing-key、x-message-ttl。
package com.fzb.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)
 * @Author jxb
 * @Date 2019-03-10 10:25:30
 */
@Component
public class MQDelayConfig {

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

    /**
     * @Description 定义支付队列 绑定死信队列(其实是绑定的交换器,然后通过交换器路由键绑定队列) 设置过期时间
     * @Author jxb
     * @Date 2019-04-02 14:40:24
     */
    @Bean
    private Queue directPayQueue() {
        Map 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 jxb
     * @Date 2019-04-02 14:46:10
     */
    @Bean
    private Binding bindingOrderDirect() {
        return BindingBuilder.bind(directPayQueue()).to(directPayExchange()).with("OrderPay");
    }
}
  • 带有过期时间且绑定死信交换器的队列
RabbitMQ(5)SpringBoot+RabbitMQ用死信队列和插件形式实现延迟队列_第3张图片
队列
  • 生产者,为消息设置过期时间setExpiration("15000");
    /**
     * @Description 支付队列、绑定死信队列,测试消息延迟功能
     * @Author jxb
     * @Date 2019-04-02 14:07:25
     */
    @RequestMapping(value = "/directDelayMQ", method = {RequestMethod.GET})
    public List directDelayMQ() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        List 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 jxb
     * @Date 2019-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);
    }

测试结果,因为消息配置的是15秒后到期,而队列配置了10秒到期,所以最终按照时间短的计算。

RabbitMQ(5)SpringBoot+RabbitMQ用死信队列和插件形式实现延迟队列_第4张图片
延迟队列

思考:如果先放入一条A消息过期时间是10秒,再放入一个b消息过期时间是5秒,那延迟队列是否可以先消费b消息?
答案是否定的,因为队列就会遵循先进先出的规则,b消息会等a消息过期后,一起消费,这就是所谓的队列阻塞。由这个问题我们引出插件形式来实现延迟队列

2. 用rabbitmq-delayed-message-exchange插件实现延迟队列
  • 下载插件
    https://bintray.com/rabbitmq/community-plugins/rabbitmq_delayed_message_exchange
  • 强烈建议安装erlang20+版本和RabbitMQ3.7+版本,另插件版本要和RabbitMQ版本一致。
  • 解压成.ez的文件,上传到RabbitMQ安装目录的plugins文件夹下,停止服务器,开启插件,启动服务器。
1. 查看yum 安装的软件路径
   查找安装包:rpm -qa|grep rabbitmq
   查找位置: rpm -ql rabbitmq-server-3.6.15-1.el6.noarch
   卸载yum安装:yum remove rabbitmq-server-3.6.15-1.el6.noarch
2. 上传到plugins文件夹
3. 停止服务器
   service rabbitmq-server stop
4. 开启插件
   rabbitmq-plugins enable rabbitmq_delayed_message_exchange
   (关闭插件)
   rabbitmq-plugins disable rabbitmq_delayed_message_exchange
5. 启动服务器
   service rabbitmq-server start
6. 查看插件
   rabbitmq-plugins list
  • 生产者,设置Header属性x-delay过期时间
    /**
     * @Description 插件延迟队列功能
     * @Author jxb
     * @Date 2019-04-02 14:07:25
     */
    @RequestMapping(value = "/directPluginDelayMQ", method = {RequestMethod.GET})
    public List directPluginDelayMQ() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        List users = userService.getUserList(null);
        for (User user : users) {
            CorrelationData correlationData = new CorrelationData(String.valueOf(user.getId()));
            rabbitTemplate.convertAndSend("direct.plugin.delay.exchange", "PluginDelayKey", user,
                    message -> {
                        // 设置5秒过期
                        message.getMessageProperties().setHeader("x-delay",5000);
                        return message;
                    },
                    correlationData);
            System.out.println(user.getName() + ":" + sdf.format(new Date()));
        }
        return users;
    }
  • 消费者,设置x-delayed-message类型的交换器,增加参数x-delayed-type为direct
    /**
     * @Description 插件延迟队列
     * @Author jxb
     * @Date 2019-04-04 16:34:28
     */
    @RabbitListener(bindings = {@QueueBinding(value = @Queue(value = "direct.plugin.delay.queue"), exchange = @Exchange(value = "direct.plugin.delay.exchange",type = "x-delayed-message",arguments = {@Argument(name="x-delayed-type",value = "direct")}), key = {"PluginDelayKey"})})
    public void getPDLMessage(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);
    }
  • 插件形式交换器
RabbitMQ(5)SpringBoot+RabbitMQ用死信队列和插件形式实现延迟队列_第5张图片
交换器

注:用代码是创建一个:CustomExchange自定义交换器,类型一定要设置成:x-delayed-message
注:如果配置了发送回调ReturnCallback,插件延迟队列则会回调该方法,因为发送方确实没有投递到队列上,只是在交换器上暂存,等过期时间到了 才会发往队列。

RabbitMQ(5)SpringBoot+RabbitMQ用死信队列和插件形式实现延迟队列_第6张图片
消息暂存在交换器
  • SpringBoot集成RabbitMQ常用配置(非本系列用)
#rabbitmq
spring.rabbitmq.host=192.168.89.168
spring.rabbitmq.port=5672
spring.rabbitmq.username=fzb
spring.rabbitmq.password=fzb2019
spring.rabbitmq.virtual-host=fzb_host
#消费者数量
spring.rabbitmq.listener.simple.concurrency=10
#最大消费者数量
spring.rabbitmq.listener.simple.max-concurrency=10
#消费者每次从队列获取的消息数量。写多了,如果长时间得不到消费,数据就一直得不到处理
spring.rabbitmq.listener.simple.prefetch=1
#消费者自动启动
spring.rabbitmq.listener.simple.auto-startup=true
#消费者消费失败,自动重新入队
spring.rabbitmq.listener.simple.default-requeue-rejected=true
#启用发送重试 队列满了发不进去时启动重试
spring.rabbitmq.template.retry.enabled=true 
#1秒钟后重试一次
spring.rabbitmq.template.retry.initial-interval=1000 
#最大重试次数 3次
spring.rabbitmq.template.retry.max-attempts=3
#最大间隔 10秒钟
spring.rabbitmq.template.retry.max-interval=10000
#等待间隔 的倍数。如果为2  第一次 乘以2 等1秒, 第二次 乘以2 等2秒 ,第三次 乘以2 等4秒
spring.rabbitmq.template.retry.multiplier=1.0

做一个有趣的人,让生活更好玩一些

你可能感兴趣的:(RabbitMQ(5)SpringBoot+RabbitMQ用死信队列和插件形式实现延迟队列)