RabbitMQ(六)延时队列
文章目录
7 延迟队列
延迟队列,队列的内部是有序的,最终要的特性体现在延时属性上,延时队里中的元素是希望在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间后被处理的元素的队列。
7.1 延迟队列使用场景
- 订单在十分钟之内未支付则自动取消。
- 新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
- 用户注册成功后,如果三天内没有登录则进行短信提醒。
- 用户发起退款,若果三天内没有得到处理则通知相关运营人员。
- 预定会议后,需要在预定时间点的前十分钟通知各个参与会议人员。
这些场景都有一个特点,需要在某个事件发生之后或者之前的指定时间点完成某一项任务,如:
发生订单生成事件,在十分钟之后检查该订单支付状态,然后将未支付的订单进行关闭;看起来似乎 使用定时任务,一直轮询数据,每秒查一次,取出需要被处理的数据,然后处理不就完事了吗?如果 数据量比较少,确实可以这样做,比如:对于“如果账单一周内未支付则进行自动结算”这样的需求,如果对于时间不是严格限制,而是宽松意义上的一周,那么每天晚上跑个定时任务检查一下所有未支付的账单,确实也是一个可行的方案。但对于数据量比较大,并且时效性较强的场景,如:“订单十分钟内未支付则关闭“,短期内未支付的订单数据可能会有很多,活动期间甚至会达到百万甚至千万 级别,对这么庞大的数据量仍旧使用轮询的方式显然是不可取的,很可能在一秒内无法完成所有订单 的检查,同时会给数据库带来很大压力,无法满足业务要求而且性能低下。
7.2 RabbitMQ中的TTL
TTL:TTL是RabbitMQ中一个消息或者队列的属性,表明一条消息或者该队列中所有消息的最大存活时间,单位是毫秒,如果一条消息设置了TTL属性或者进入了设置TTL属性的队列,那么这条消息如果在TTL设置的时间内没有被消费,就会成为死信
,如果同时设置了队列的TTL和消息,那么较小的那个值会被使用,有两种方式设置TTL
7.3 设置TTL
有两种方式设置TTL,一种是消息设置TTL,一种是队列设置TTL。
7.3.1 消息设置TTL
//设置ttl过期时间
rabbitTemplate.convertAndSend("X", "XC", "消息来自ttl为" + Long.parseLong(ttlTime) / 1000 + "s 的队列:" + message,
msg -> {
//设置发送消息的延时时长
msg.getMessageProperties().setExpiration(ttlTime);
return msg;
});
7.3.2 队列设置TTL
声明时设置队列的x-message-ttl
属性
//设置ttl过期时间
argument.put("x-message-ttl", 10000);
return QueueBuilder.durable(QUEUE_A).withArguments(argument).build();
7.3.3 两者区别
如果设置了队列的 TTL 属性,那么一旦消息过期,就会被队列丢弃(如果配置了死信队列被丢到死信队列中),
而消息设置TTL方式,消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的,如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长时间;
另外,还需 要注意的一点是,如果不设置 TTL,表示消息永远不会过期,如果将 TTL 设置为 0,则表示除非此时可以直接投递该消息到消费者,否则该消息将会被丢弃。
7.4 RabbitMQ整合SpringBoot
7.4.1 创建Springboot项目
7.4.2 添加maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--MQ依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>1.5.21</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.6</version>
</dependency>
<!--RabbitMQ 测试依赖-->
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
7.4.3 修改application.yml
spring:
rabbitmq:
host: MQ主机ip
port: 5672 MQ的端口号
username: MQ账号
password: MQ密码
server:
port: 9191 代码运行的端口号
7.4.4 添加Swagger配置类
/**
* Created by IntelliJ IDEA.
* User: LvHaoIT (lvhao)
* Date: 2022/7/21
* Time: 11:41
*/
@Configuration
@EnableSwagger2
@EnableSwaggerBootstrapUI
public class SwaggerConfig extends WebMvcConfigurationSupport {
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").
addResourceLocations("classpath:/static/");
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("doc.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
// 原生的界面
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("top.lvhaoit.rabbit.springbootrabbitmq"))
.paths(PathSelectors.any())
.build()
//不需要时,或者生产环境可以在此处关闭
.enable(true);
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Springboot整合RabbitMQ ")
.description("描述:项目接口!")
//服务条款网址
.termsOfServiceUrl("https://blog.csdn.net/qq_27331467")
.contact("lvhaoit")
.version("1.0")
.build();
}
@Bean
public Docket adminApiConfig() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("adminApi")
.apiInfo(adminApiInfo())
.select()
//该位置输入需要检查的controller路径,可以添加多个。
.apis(RequestHandlerSelectors.basePackage("top.lvhaoit.rabbit.springbootrabbitmq"))
.paths(PathSelectors.any())
.build()
//不需要时,或者生产环境可以在此处关闭
.enable(true);
}
private ApiInfo adminApiInfo() {
return new ApiInfoBuilder()
.title("Springboot整合RabbitMQ-接口文档 ")
.description("描述:项目接口!")
//服务条款网址
.termsOfServiceUrl("https://blog.csdn.net/qq_27331467")
.contact("lvhaoit")
.version("1.0")
.build();
}
}
7.4.5 修改启动类
主要是在启动类上加上 @EnableSwagger2
注解`
@SpringBootApplication
@EnableSwagger2
@Slf4j
public class SpringbootRabbitMqApplication {
public static void main(String[] args) throws UnknownHostException {
ConfigurableApplicationContext application = SpringApplication.run(SpringbootRabbitMqApplication.class, args);
Environment env = application.getEnvironment();
String ip = InetAddress.getLocalHost().getHostAddress();
String port = env.getProperty("server.port");
String path = env.getProperty("server.servlet.context-path");
if (path == null) path = "";
log.info("\n----------------------------------------------------------\n\t" +
"LxApplication is running! Access URLs:\n\t" +
"Local: \t\thttp://localhost:" + port + path + "/\n\t" +
"swagger-ui: \thttp://" + ip + ":" + port + path + "/swagger-ui.html\n\t" +
"rabbitMQ: \thttp://" + "www.lvhaoit.top:15672\n\t" +
"Doc: \t\thttp://" + ip + ":" + port + path + "/doc.html\n\t" +
"----------------------------------------------------------");
}
}
7.5 队列TTL
7.5.1 代码架构图
创建两个队列QA和QB,两者队列分别设置10s和40s,然后再创建一个交换机X和死信交换机Y,他们的类型都是direct,创建一个死信队列QD,他们的绑定关系如下:
7.5.2 实战代码
-
结构配置类代码
/** * TTL队列 配置文件类代码 */ @Configuration public class TtlQueueConfig { //普通交换机名称 public static final String X_EXCHANGE = "X"; //死信交换机名称 public static final String Y_DEAD_LETTER_EXCHANGE = "Y"; //普通队列名称 public static final String QUEUE_A = "QA"; public static final String QUEUE_B = "QB"; //死信队列名称 public static final String QUEUE_DEAD_LETTER_D = "QD"; //声明xExchange 别名 @Bean("xExchange") public DirectExchange xExchange() { return new DirectExchange(X_EXCHANGE); } //声明xExchange 别名 @Bean("yExchange") public DirectExchange yExchange() { return new DirectExchange(Y_DEAD_LETTER_EXCHANGE); } //声明普通队列TTL为10s @Bean("queueA") public Queue queueA() { HashMap<String, Object> argument = new HashMap<>(); //设置死信交换机 argument.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE); //设置死信RoutingKey argument.put("x-dead-letter-routing-key", "YD"); //设置ttl过期时间 argument.put("x-message-ttl", 10000); return QueueBuilder.durable(QUEUE_A).withArguments(argument).build(); } //声明普通队列TTL为40s @Bean("queueB") public Queue queueB() { HashMap<String, Object> argument = new HashMap<>(); //设置死信交换机 argument.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE); //设置死信RoutingKey argument.put("x-dead-letter-routing-key", "YD"); //设置ttl过期时间 argument.put("x-message-ttl", 40000); return QueueBuilder.durable(QUEUE_B).withArguments(argument).build(); } //声明死信队列 @Bean("queueD") public Queue queueD() { return QueueBuilder.durable(QUEUE_DEAD_LETTER_D).build(); } //绑定关系 @Bean public Binding queueABindingX(@Qualifier("queueA") Queue queue, @Qualifier("xExchange") DirectExchange exchange) { return BindingBuilder.bind(queue).to(exchange).with("XA"); } @Bean public Binding queueBBindingX(@Qualifier("queueB") Queue queue, @Qualifier("xExchange") DirectExchange exchange) { return BindingBuilder.bind(queue).to(exchange).with("XB"); } @Bean public Binding queueDBindingY(@Qualifier("queueD") Queue queue, @Qualifier("yExchange") DirectExchange exchange) { return BindingBuilder.bind(queue).to(exchange).with("YD"); } }
-
消息消费者代码
@Slf4j @Component public class DeadLetterQueueConsumer { //接收消息 @RabbitListener(queues = "QD") public void receiveD(Message message, Channel channel) throws Exception { String msg = new String(message.getBody(), "UTF-8"); log.info("当前时间: {}.收到死信队列的消息: {}", new Date(), msg); } }
-
消息生产者代码
@Slf4j @Api(tags = "TTL延迟队列") @RestController @RequestMapping("ttl") public class SendMsgController { @Resource private RabbitTemplate rabbitTemplate; //开始发送消息 @GetMapping("/sendMsg/{message}") @ApiOperation(value = "发送两条消息到延迟队列", notes = "接口描述") public void sendMsg(@PathVariable String message) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss"); log.info("当前时间:{},发送一条信息给两个TTL队列:{}", sdf.format(new Date()), message); rabbitTemplate.convertAndSend("X", "XA", "消息来自ttl为10s的队列:" + message); rabbitTemplate.convertAndSend("X", "XB", "消息来自ttl为40s的队列:" + message); } }
发起一个请求 http://localhost:9191/ttl/sendMsg/测试消息
第一条发送的消息在10s后变成了死信,然后被消费者消费掉,第二条发送的消息在40s后变成了死信消息,然后被消费掉,已经达成一个延时队列。
思考:如果这样的话,岂不是每增加一个新的时间需求,就需要新增一个队列,这里只有10s和40s两个时间选项,如果需要一个小时后处理,那么就需要增加TTL为一个小时的队列,这样如果是预定会议然后提前通知的场景,岂不是要增加无数个队列才能满足需求?
7.6 延时队列优化
通过上面的TTL例子,我们发现如果TTL是一个固定的值,那么延迟队列使用起来会非常不灵活。所以在这里我们新增一个队列QC,不设置TTL时间。
7.6.1 代码架构图
7.6.2 实战代码
-
结构配置类代码(在上一个的基础上加上QC队列的配置与绑定关系)
//设置一个时间不固定的延迟队列 @Bean("queueC") public Queue queueC() { HashMap<String, Object> argument = new HashMap<>(); //设置死信交换机 argument.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE); //设置死信RoutingKey argument.put("x-dead-letter-routing-key", "YD"); return QueueBuilder.durable(QUEUE_C).withArguments(argument).build(); } //设置一个绑定关系 @Bean public Binding queueCBindingX(@Qualifier("queueC") Queue queue, @Qualifier("xExchange") DirectExchange exchange) { return BindingBuilder.bind(queue).to(exchange).with("XC"); }
-
消息生产者代码(通过在发消息的时候设置动态TTL值)
@GetMapping("/sendExpirationMsg/{message}/{ttlTime}") @ApiOperation(value = "不定时延迟队列", notes = "接口描述") public boolean sendExpMsg(@PathVariable String message, @PathVariable String ttlTime) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss"); log.info("当前时间:{},发送一条信息给不定时TTL队列:{} ,TTL的值为{}", sdf.format(new Date()), message, Long.parseLong(ttlTime)); rabbitTemplate.convertAndSend("X", "XC", "消息来自ttl为" + Long.parseLong(ttlTime) / 1000 + "s 的队列:" + message, msg -> { //设置发送消息的延时时长 msg.getMessageProperties().setExpiration(ttlTime); return msg; }); return true; }
发起两个请求
-
设置TTL为两秒的消息
-
设置TTL为二十秒的消息
这样看起来似乎没什么问题,但是如果在消息属性上设置TTL的方式,消息可能并不会按时死亡,因为rabbitMQ只会检查第一个消息是否过期,如果过期则放入死信队列,如果第一个消息的延时时长很长,而第二个延时时间很短,第二个消息并不会优先被消费。
7.7 RabbitMQ插件实现延迟队列
刚才提到的问题,如果不能实现消息粒度上的TTL,并让他在设置的TTL及时死亡,就无法设计成一个通用的延迟队列。
这个问题是MQ的一个问题,可以通过安装延时队列插件来解决。
7.7.1 安装延时队列插件
下载插件rabbitmq_delayed_message_exchange
注意版本对应
下载好后,将它放到RabbitMQ的plgins目录下,执行下面命令让插件生效,然后重启RabbitMQ。
# 安装插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
# 停止 rabbitmq
rabbitmqctl stop
# 后台启动 rabbitmq
rabbitmq-server -detached
然后在UI界面创建交换机,可以看到类型为 x-delayed-message
7.7.2 代码架构图
7.7.3 实战代码
-
结构配置类代码
在我们自定义的交换机中,这是一种新的交换机类型,该类型消息支持延迟投递机制,消息传递后并不会立即传递到目标队列中,而是存储在Mnesia(一个分布式数据系统)表中,当达到投递时间时,才会投递到目标队列中。
@Configuration public class DelayedQueueConfig { //队列 public static final String DELAYED_QUEUE_NAME = "delayed.queue"; //交换机 public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange"; //routingKey public static final String DELAYED_ROUTING_KEY = "delayed.routingKey"; //声明交换机 基于插件的交换机 @Bean public CustomExchange delayedExchange() { HashMap<String, Object> arguments = new HashMap<>(); arguments.put("x-delayed-type", "direct"); return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message",true, false, arguments); } @Bean public Queue delayedQueue() { return new Queue(DELAYED_QUEUE_NAME); } @Bean public Binding delayedQueueBindingDelayedExchange(@Qualifier("delayedQueue") Queue delayedQueue, @Qualifier("delayedExchange") CustomExchange exchange) { return BindingBuilder.bind(delayedQueue).to(exchange).with(DELAYED_ROUTING_KEY).noargs(); } }
-
消息生产者代码
@GetMapping("/sendDelayMsg/{message}/{ttlTime}") @ApiOperation(value = "插件延迟消息", notes = "接口描述") public boolean sendDelayMsg(@PathVariable String message, @PathVariable String ttlTime) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss"); log.info("当前时间:{},发送一条插件延迟消息:{} ,延迟的值为{}", sdf.format(new Date()), message, Long.parseLong(ttlTime)); rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME, DelayedQueueConfig.DELAYED_ROUTING_KEY, "消息来自延迟为" + Long.parseLong(ttlTime) / 1000 + "s 的队列:" + message, msg -> { //设置发送消息的延时时长 msg.getMessageProperties().setDelay(Integer.parseInt(ttlTime)); return msg; }); return true; }
-
消息消费者代码
/** * 延迟队列 基于插件的延迟 消费者 */ @Slf4j @Component public class DelayQueueConsumer { //监听消息 @RabbitListener(queues = DelayedQueueConfig.DELAYED_QUEUE_NAME) public void ReceiveDelayQueue(Message message) throws UnsupportedEncodingException { String msg = new String(message.getBody(), "UTF-8"); log.info("当前时间: {}.收到延迟队列的消息: {}", new Date(), msg); } }
-
发起两个请求
-
设置TTL为二十秒的消息
-
设置TTL为两秒的消息
-
第二个发的 2秒 的消息被优先消费掉了,符合我们的预期,实现了消息粒度上的延时。
7.8 总结
延时队列在需要延时处理的场景下非常有用,使用RabbitMQ来实现延时队列可以很好的利用RabbitMQ的特性,例如:消息可靠发送,消息可靠投递,死信队列来保障消费至少被消费一次以及未被正确处理的消息不会被丢弃。