RabbitMQ 延迟队列(七)

1,575 阅读3分钟

这是我参与更文挑战的第 25 天,活动详情查看: 更文挑战

日积月累,水滴石穿 😄

什么是延迟队列

延迟队列存储的是对应的延时消息,所谓”延时消息”是指当消息被发送以后,并不想让消费者立即拿到消息,而是等待指定时间后,消费者才拿到这个消息进行消费。

RabbitMQ如何实现延迟队列

AMQP 协议和 RabbitMQ 本身没有直接支持延迟队列功能,但是可以通过 DLX 和 TTL 模拟出延迟队列的功能。

实现延迟队列

需要创建两个队列,一个用于发送消息,一个用于消息过期后的消息转发的目标队列。

image.png 生产者发送消息到 Queue1 ,并且这个消息是设置了 TTL 的,比如 60s。消息会在 Queue1 中等待 60s,如果没有消费者消费的话,它会被转发到 Queue2,Queue2 有绑定消费者,处理延迟消息。

注:Queue1是没有消费者的

加入依赖

<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.0</version>
</dependency>

进行配置

spring:
  application:
    name: ttl-queue
  rabbitmq:
    host: 47.105.198.54
    port: 5672
    virtual-host: /test-1
    username: 11
    password: 111
mq:
  queueBinding:
    queue: prod_queue_pay
    dlQueue: dl-queue
    exchange:
      name: exchang_prod_pay
      dlTopicExchange: dl-topic-exchange
      type: topic
    key: prod_pay
    dlRoutingKey: dl-routing-key

创建业务队列与死信队列

@Configuration
public class RabbitConfig {
    //业务队列配置
    @Value("${mq.queueBinding.queue}")
    private String queueName;
    @Value("${mq.queueBinding.exchange.name}")
    private String exchangeMame;
    @Value("${mq.queueBinding.key}")
    private String key;
    
    //死信队列配置
    @Value("${mq.queueBinding.exchange.dlTopicExchange}")
    private String dlTopicExchange;
    @Value("${mq.queueBinding.dlRoutingKey}")
    private String dlRoutingKey;
    @Value("${mq.queueBinding.dlQueue}")
    private String dlQueue;
    
    //创建死信交换机
    @Bean
    public TopicExchange dlTopicExchange(){
        return new TopicExchange(dlTopicExchange,true,false);
    }
    //创建死信队列
    @Bean
    public Queue dlQueue(){
        return new Queue(dlQueue,true);
    }
    
    //死信队列与死信交换机进行绑定
    @Bean
    public Binding BindingErrorQueueAndExchange(Queue dlQueue, TopicExchange dlTopicExchange){
        return BindingBuilder.bind(dlQueue).to(dlTopicExchange).with(dlRoutingKey);
    }

    private final String dle = "x-dead-letter-exchange";
    private final String dlk = "x-dead-letter-routing-key";
    private final String ttl = "x-message-ttl";

    //创建业务队列
    @Bean
    public Queue payQueue(){
        Map<String,Object> params = new HashMap<>();
        //设置队列的过期时间 10s
        params.put(ttl,10000);
        //声明当前队列绑定的死信交换机
        params.put(dle,dlTopicExchange);
        //声明当前队列的死信路由键
        params.put(dlk,dlRoutingKey);
        return QueueBuilder.durable(queueName).withArguments(params).build();
    }

    //创建业务交换机
    @Bean
    public TopicExchange payTopicExchange(){
        return new TopicExchange(exchangeMame,true,false);
    }
    //业务队列与业务交换机进行绑定
    @Bean
    public Binding BindingPayQueueAndPayTopicExchange(Queue payQueue, TopicExchange payTopicExchange){
        return BindingBuilder.bind(payQueue).to(payTopicExchange).with(key);
    }
}

创建生产者

/*
 * 生产者
 */
@Component
@Slf4j
public class RabbitSender {

    @Value("${mq.queueBinding.exchange.name}")
    private String exchangeName;

    @Value("${mq.queueBinding.key}")
    private String key;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void send(String msg){
        log.info("RabbitSender.send() msg = {}",msg);
        rabbitTemplate.convertAndSend(exchangeName,key,msg);
    }
}

创建消费者

该消费者是消费死信队列中的消息

/**
 * 消费者
 */
@Component
@Slf4j
public class RabbitReceiver {

    //消费死信队列的消息
    @RabbitListener(queues = "${mq.queueBinding.dlQueue}")
    public void infoConsumption(String data) throws Exception {
        log.info("收到信息:{}",data);
        log.info("然后进行一系列逻辑处理 Thanks♪(・ω・)ノ");
    }
}

对外提供接口

@RestController
public class TestController {

    @Autowired
    private RabbitSender rabbitSender;
    @GetMapping
    public void test(@RequestParam String msg){
        rabbitSender.send(msg);
    }
}

启动服务之后,可以看到创建的交换机和队列 image.png image.png 然后进行接口调用:http://localhost:8080/?msg=哈哈哈哈哈哈

2020-11-15 00:33:02.991  RabbitSender.send() msg = 哈哈哈哈哈哈
2020-11-15 00:33:12.060  收到信息:哈哈哈哈哈哈
2020-11-15 00:33:12.060  然后进行一系列逻辑处理 Thanks♪(・ω・)ノ

我们设置的过期时间为 10s;消息过期之后变为了死信。从prod_queue_pay队列转发到dl-queue队列。感觉很完美的进行了实现。

但我们会发现一个问题,如果我现在有不同的场景,比如分别5s、10s、15s之后延迟消费,那就需要创建三个队列。每次有一个不同的时间段的需求过来,我都需要创建一个队列,这肯定不行。

RabbitMQ插件实现延迟队列

既然遇到问题那就要解决它!首先来安装一个插件。

快速入口:juejin.cn/post/697714…

创建队列和交换机

@Configuration
public class RabbitConfig2 {

    private static final String EXCHANGE_NAME = "delayed_exchange";
    private static final String QUEUE_NAME = "delayed_queue";
    private static final String ROUTE_KEY = "delayed_key";
    /**
     * 交换机
     */
    @Bean
    CustomExchange  exchange() {
        //通过x-delayed-type参数设置fanout /direct / topic / header 类型
        Map<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "topic");
        return new CustomExchange(EXCHANGE_NAME, "x-delayed-message",true, false,args);
    }

    /**
     * 队列
     */
    @Bean
    public Queue queue() {
        return new Queue(QUEUE_NAME,true,false,false);
    }

    /**
     * 将队列绑定到交换机
     */
    @Bean
    public Binding binding(CustomExchange exchange,Queue queue) {
        return BindingBuilder
                .bind(queue)
                .to(exchange)
                .with(ROUTE_KEY)
                .noargs();
    }
}

创建生产者

发送消息的时候通过在header添加 x-delay 参数来控制消息的延时时间。

/*
 * 生产者
 */
@Component
@Slf4j
public class RabbitSender {
    private static final String ROUTE_KEY = "delayed_key";
    private static final String EXCHANGE_NAME = "delayed_exchange";
    /**
     * @param msg 消息
     * @param delay   延时时间,秒
     */
    public void send2(String msg,int delay){
        log.info("RabbitSender.send() msg = {}",msg);
        rabbitTemplate.convertAndSend(EXCHANGE_NAME, ROUTE_KEY, msg, message ->{
            message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);  //消息持久化
            message.getMessageProperties().setDelay(delay * 1000);   // 单位为毫秒
            return message;
        });
    }
}


   // setDelay方法的源码实现
   public void setDelay(Integer delay) {
        if (delay != null && delay >= 0) {
            this.headers.put("x-delay", delay);
        } else {
            this.headers.remove("x-delay");
        }

    }

创建消费者

/**
 * 消费者
 */
@Component
@Slf4j
public class RabbitReceiver {

    @RabbitListener(queues = "delayed_queue")
    public void infoConsumption(String data) throws Exception {
        log.info("收到信息:{}",data);
        log.info("然后进行一系列逻辑处理 Thanks♪(・ω・)ノ");
    }
}

提供对外方法

@RestController
public class TestController {

    @Autowired
    private RabbitSender rabbitSender;

    @GetMapping("/test2/{msg}/{delay}")
    public void test2(@PathVariable("msg") String msg, @PathVariable("delay")int delay){
        rabbitSender.send2(msg,delay);
    }
}

启动服务,登录 RabbitMQ 管理界面,可以看到交换机和队列都已经创建成功。 image.png image.png image.png 然后分别发送延迟消费时间为60s、30s、5s的消息。查看消费者的消费记录。

请求1:http://localhost:8080/test2/msg=发送时间为60s的过期时间/60

请求2:http://localhost:8080/test2/msg=发送时间为30s的过期时间/30

请求3:http://localhost:8080/test2/msg=发送时间为5s的过期时间/5

通过打印的日志可以发现:依次发送60s、30s、5s。但消费的顺序为5s、30s、60s。

2020-11-15 16:13:58.783  RabbitSender.send() msg = msg=发送时间为60s的过期时间
2020-11-15 16:14:02.653  RabbitSender.send() msg = msg=发送时间为30s的过期时间
2020-11-15 16:14:08.880  RabbitSender.send() msg = msg=发送时间为5s的过期时间
2020-11-15 16:14:13.924  收到信息:msg=发送时间为5s的过期时间
2020-11-15 16:14:13.925  然后进行一系列逻辑处理 Thanks♪(・ω・)ノ
2020-11-15 16:14:32.685  收到信息:msg=发送时间为30s的过期时间
2020-11-15 16:14:32.687  然后进行一系列逻辑处理 Thanks♪(・ω・)ノ
2020-11-15 16:14:58.814  收到信息:msg=发送时间为60s的过期时间
2020-11-15 16:14:58.814  然后进行一系列逻辑处理 Thanks♪(・ω・)ノ
  • 如你对本文有疑问或本文有错误之处,欢迎评论留言指出。如觉得本文对你有所帮助,欢迎点赞和关注。