RabbitMQの延迟消息

1、什么是延时队列

首先,它是一种队列,队列意味着内部的元素是有序的,元素出队和入队是有方向性的,元素从一端进入,从另一端取出。

其次,延时队列,最重要的特性就体现在它的延时属性上,跟普通的队列不一样的是,普通队列中的元素总是等着希望被早点取出处理,而延时队列中的元素则是希望被在指定时间得到取出和处理,所以延时队列中的元素是都是带时间属性的,通常来说是需要被处理的消息或者任务。

简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。

2、延时队列使用场景

考虑一下以下场景:

订单在十分钟之内未支付则自动取消。
新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
账单在一周内未支付,则自动结算。
用户注册成功后,如果三天内没有登陆则进行短信提醒。
用户发起退款,如果三天内没有得到处理则通知相关运营人员。
预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议。
这些场景都有一个特点,需要在某个事件发生之后或者之前的指定时间点完成某一项任务,如:发生订单生成事件,在十分钟之后检查该订单支付状态,然后将未支付的订单进行关闭;发生店铺创建事件,十天后检查该店铺上新商品数,然后通知上新数为0的商户;发生账单生成事件,检查账单支付状态,然后自动结算未支付的账单;发生新用户注册事件,三天后检查新注册用户的活动数据,然后通知没有任何活动记录的用户;发生退款事件,在三天之后检查该订单是否已被处理,如仍未被处理,则发送消息给相关运营人员;发生预定会议事件,判断离会议开始是否只有十分钟了,如果是,则通知各个与会人员。

看起来似乎使用定时任务,一直轮询数据,每秒查一次,取出需要被处理的数据,然后处理不就完事了吗?如果数据量比较少,确实可以这样做,比如:对于“如果账单一周内未支付则进行自动结算”这样的需求,如果对于时间不是严格限制,而是宽松意义上的一周,那么每天晚上跑个定时任务检查一下所有未支付的账单,确实也是一个可行的方案。但对于数据量比较大,并且时效性较强的场景,如:“订单十分钟内未支付则关闭“,短期内未支付的订单数据可能会有很多,活动期间甚至会达到百万甚至千万级别,对这么庞大的数据量仍旧使用轮询的方式显然是不可取的,很可能在一秒内无法完成所有订单的检查,同时会给数据库带来很大压力,无法满足业务要求而且性能低下。

3、RabbitMQ中的TTL

TTL(Time To Live)是什么呢?TTL是RabbitMQ中一个消息或者队列的属性,表明一条消息或者该队列中的所有消息的最大存活时间,单位是毫秒。换句话说,如果一条消息设置了TTL属性或者进入了设置TTL属性的队列,那么这条消息如果在TTL设置的时间内没有被消费,则会成为“死信”。如果同时配置了队列的TTL和消息的TTL,那么较小的那个值将会被使用。

4、如果使用RabbitMQ来实现延时队列

4.1、安装插件

从这里 https://www.rabbitmq.com/community-plugins.html 下载rabbitmq_delayed_message_exchange插件,然后解压放置到RabbitMQ的插件目录。

接下来,进入RabbitMQ的安装目录下的sbin目录,执行下面命令让该插件生效,然后重启RabbitMQ。

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

4.2、代码实现

4.2.1、新建maven工程,pom.xml文件

<?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.7.15</version>
        <relativePath/>
    </parent>
    <groupId>com.cnblogs.javalouvre</groupId>
    <artifactId>delayed_message_notice</artifactId>
    <version>0.0.1</version>
    <name>延迟消息推送</name>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <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-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

4.2.2、属性配置文件application.yml

spring:
  rabbitmq:
    host: localhost
    port: 5672
    virtual-host: /mall
    username: mall
    password: mall
    listener:
      type: simple
      simple:
        default-requeue-rejected: false
        acknowledge-mode: manual

4.2.3、定义常量

package com.cnblogs.javalouvre.common;

public interface Constants {

    String DELAYED_QUEUE = "delay.queue.demo.delay.queue";
    String DELAYED_EXCHANGE = "delay.queue.demo.delay.exchange";
    String DELAYED_ROUTING_KEY   = "delay.queue.demo.delay.routingkey";

}

4.2.3、配置RabbitMQ

package com.cnblogs.javalouvre.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.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

import static com.cnblogs.javalouvre.common.Constants.*;

@Configuration
public class RabbitMQConfig {

    @Bean
    public Queue immediateQueue() {
        return new Queue(DELAYED_QUEUE);
    }

    @Bean
    public CustomExchange customExchange() {
        final Map<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "direct");
        return new CustomExchange(DELAYED_EXCHANGE, "x-delayed-message", true, false, args);
    }

    @Bean
    public Binding bindingNotify(
            @Qualifier("immediateQueue") Queue queue,
            @Qualifier("customExchange") CustomExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with(DELAYED_ROUTING_KEY).noargs();
    }

}

4.2.4、定义重试时间枚举类型

package com.cnblogs.javalouvre.enums;

@lombok.Getter
@lombok.RequiredArgsConstructor
public enum RetryIntervalEnum {

    /**
     * 第1次,间隔0秒
     */
    C0((byte) 0, 0),

    /**
     * 重试第1次,间隔1分钟
     */
    C1((byte) 1, 60000),

    /**
     * 重试第2次,间隔2分钟
     */
    C2((byte) 2, 120000),

    /**
     * 重试第3次,间隔4分钟
     */
    C3((byte) 3, 240000),

    /**
     * 重试第4次,间隔1小时
     */
    C4((byte) 4, 600000),

    /**
     * 重试第5次,间隔1小时
     */
    C5((byte) 5, 3600000),

    /**
     * 重试第6次,间隔2小时
     */
    C6((byte) 6, 7200000),

    /**
     * 重试第7次,间隔6小时
     */
    C7((byte) 7, 21600000),

    /**
     * 重试第8次,间隔15小时
     */
    C8((byte) 8, 54000000);

    private final byte count;
    private final int delay;
}

4.2.5、定义消息消费者

package com.cnblogs.javalouvre.mq;

import com.cnblogs.javalouvre.common.Constants;
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.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;

import java.io.IOException;

import static java.nio.charset.StandardCharsets.UTF_8;

@Slf4j
@Component
public class MessageReceiver {

    private final RabbitTemplate rabbitTemplate;

    public MessageReceiver(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }

    /**
     * 消费消息
     *
     * @param message 消息
     * @param channel 通道
     */
    @RabbitListener(queues = Constants.DELAYED_QUEUE, concurrency = "5-8")
    public void execute(Message message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {
        String msg = new String(message.getBody(), UTF_8);

        log.info("消费消息:{}, {}", msg, deliveryTag);
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }

}

4.2.6、定义生产者

package com.cnblogs.javalouvre.mq;

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

import static com.cnblogs.javalouvre.common.Constants.DELAYED_EXCHANGE;
import static com.cnblogs.javalouvre.common.Constants.DELAYED_ROUTING_KEY;


@Component
public class MessageSender {

    private final RabbitTemplate rabbitTemplate;

    public MessageSender(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }

    /**
     * 发送消息
     *
     * @param message 消息内容
     * @param delay   延迟时间(单位:毫秒)
     */
    public void execute(Object message, Integer delay) {
        this.rabbitTemplate.convertAndSend(DELAYED_EXCHANGE, DELAYED_ROUTING_KEY, message, mpp -> {
            mpp.getMessageProperties().setDelay(delay);
            return mpp;
        });
    }
}

4.2.7、生产消息

package com.cnblogs.javalouvre.web;

import com.cnblogs.javalouvre.mq.MessageSender;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class IndexController {

    private final MessageSender sender;

    public IndexController(MessageSender sender) {
        this.sender = sender;
    }

    @RequestMapping("delayMsg")
    public void delayMsg(String msg, Integer delayTime) {
        for (int i = 0; i < 1000; i++) {
            this.sender.execute(msg + "-" +  i, delayTime);
        }
    }

}

参考:https://www.imooc.com/article/290030

posted @ 2023-12-20 15:53  Bruce.Chang.Lee  阅读(7)  评论(0编辑  收藏  举报