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);
}
}
}
薔薇猛虎皆成個性,陽光雨露俱是天恩!