RabbitMQ(五)延时任务
延时队列
顾名思义,延迟队列就是进入该队列的消息会被延迟消费的队列。而一般的队列,消息一旦入队了之后就会被消费者马上消费。
场景一:在订单系统中,一个用户下单之后通常有30分钟的时间进行支付,如果30分钟之内没有支付成功,那么这个订单将进行一场处理。这是就可以使用延时队列将订单信息发送到延时队列。
场景二:用户希望通过手机远程遥控家里的智能设备在指定的时间进行工作。这时候就可以将用户指令发送到延时队列,当指令设定的时间到了再将指令推送到只能设备。
场景
延迟队列多用于需要延迟工作的场景。最常见的是以下两种场景:
1、延迟消费。
比如:
- 用户生成订单之后,需要过一段时间校验订单的支付状态,如果订单仍未支付则需要及时地关闭订单。
- 用户注册成功之后,需要过一段时间比如一周后校验用户的使用情况,如果发现用户活跃度较低,则发送邮件或者短信来提醒用户使用。
2、延迟重试。
比如消费者从队列里消费消息时失败了,但是想要延迟一段时间后自动重试。
如果不使用延迟队列,那么我们只能通过一个轮询扫描程序去完成。这种方案既不优雅,也不方便做成统一的服务便于开发人员使用。但是使用延迟队列的话,我们就可以轻而易举地完成。
实现
Rabbitmq实现延时队列一般而言有两种形式:
- 第一种方式:利用两个特性:
Time To Live(TTL)
、Dead Letter Exchanges(DLX)
- 第二种方式:利用
rabbitmq
中的插件x-delay-message
方式一:Dead Letter
实现思路
在介绍具体的实现思路之前,我们先来介绍一下RabbitMQ的两个特性,一个是Time-To-Live Extensions
,另一个是Dead Letter Exchanges
。
RabbitMQ
允许我们为消息或者队列设置TTL
(time to live),也就是过期时间。TTL
表明了一条消息可在队列中存活的最大时间,单位为毫秒。也就是说,当某条消息被设置了TTL
或者当某条消息进入了设置了TTL
的队列时,这条消息会在经过TTL
秒后“死亡”,成为Dead Letter
。如果既配置了消息的TTL
,又配置了队列的TTL
,那么较小的那个值会被取用。更多资料请查阅官方文档。
刚才提到了,被设置了TTL
的消息在过期后会成为Dead Letter(死信)
。其实在RabbitMQ
中,一共有三种消息的“死亡”形式:
- 消息被拒绝。通过调用
basic.reject
或者basic.nack
并且设置的requeue
参数为false
。 - 消息因为设置了
TTL
而过期。 - 消息进入了一条已经达到最大长度的队列。
如果队列设置了Dead Letter Exchange
(DLX),那么这些Dead Letter
就会被重新publish到Dead Letter Exchange
,通过Dead Letter Exchange
路由到其他队列。
RabbitMQ的Queue
可以配置x-dead-letter-exchange
和x-dead-letter-routing-key
(可选)两个参数,如果队列内出现了dead letter
,则按照这两个参数重新路由转发到指定的队列。
-
x-dead-letter-exchange:出现dead letter之后将dead letter重新发送到指定exchange。
-
x-dead-letter-routing-key:出现dead letter之后将dead letter重新按照指定的routing-key发送。
流程图
聪明的你肯定已经想到了,如何将RabbitMQ的TTL和DLX特性结合在一起,实现一个延迟队列。
针对于上述的延迟队列的两个场景,我们分别有以下两种流程图:
延迟消费
延迟消费是延迟队列最为常用的使用模式。如下图所示,生产者产生的消息首先会进入缓冲队列(图中红色队列)。通过RabbitMQ提供的TTL扩展,这些消息会被设置过期时间,也就是延迟消费的时间。等消息过期之后,这些消息会通过配置好的DLX转发到实际消费队列(图中蓝色队列),以此达到延迟消费的效果。
延迟重试
延迟重试本质上也是延迟消费的一种,但是这种模式的结构与普通的延迟消费的流程图较为不同,所以单独拎出来介绍。
如下图所示,消费者发现该消息处理出现了异常,比如是因为网络波动引起的异常。那么如果不等待一段时间,直接就重试的话,很可能会导致在这期间内一直无法成功,造成一定的资源浪费。那么我们可以将其先放在缓冲队列中(图中红色队列),等消息经过一段的延迟时间后再次进入实际消费队列中(图中蓝色队列),此时由于已经过了“较长”的时间了,异常的一些波动通常已经恢复,这些消息可以被正常地消费。
代码实现
接下来我们将介绍如何在Spring Boot中实现基于RabbitMQ的延迟队列。我们假设读者已经拥有了Spring Boot与RabbitMQ的基本知识。
初始化工程
首先我们在Intellij中创建一个Spring Boot工程,并且添加spring-boot-starter-amqp
扩展。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--fast json依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.40</version>
</dependency>
配置application.yml
server:
port: 8088
spring:
application:
name: rabbitmq_delay
rabbitmq:
host: 192.168.3.253
port: 5672
username: admin
password: 123456
publisher-confirms: true
listener:
simple:
concurrency: 10 #并发消费者的初始化值
max-concurrency: 20 #并发消费者的最大值
prefetch: 5 #每个消费者每次监听时可拉取处理的消息数量。
配置队列
添加一个QueueEnum
队列枚举配置,该枚举内配置队列的Exchange
、QueueName
、RouteKey
等相关内容,如下所示:
package com.lay.rabbitmqdelay.config;
/**
* @Description:消息队列枚举配置
* @Author: lay
* @Date: Created in 19:31 2018/12/20
* @Modified By:IntelliJ IDEA
*/
public enum QueueEnum {
//延迟消息通知队列
MESSAGE_QUEUE("message.center.direct", "message.create", "message_center.create"),
//消息通知ttl队列
MESSAGE_TTL_QUEUE("message.center.topic.ttl", "message.create.ttl", "message_center.create.ttl");
//交换机名称
private String exchange;
//队列名称
private String name;
//路由键
private String routingKey;
QueueEnum(String exchange, String name, String routingKey) {
this.exchange = exchange;
this.name = name;
this.routingKey = routingKey;
}
public String getExchange() {
return exchange;
}
public String getName() {
return name;
}
public String getRoutingKey() {
return routingKey;
}
}
可以看到MESSAGE_QUEUE
队列配置跟我们之前章节的配置一样,而我们另外新创建了一个后缀为ttl
的消息队列配置。我们采用的这种方式是RabbitMQ
消息队列其中一种的延迟消费模块,通过配置队列消息过期后转发的形式。
这种模式比较简单,我们需要将消息先发送到
ttl
延迟队列内,当消息到达过期时间后会自动转发到ttl
队列内配置的转发Exchange
以及RouteKey
绑定的队列内完成消息消费。
下面我们来模拟消息通知
的延迟消费场景,先来创建一个名为MessageRabbitMqConfiguration
的队列配置类,该配置类内添加消息通知队列
配置以及消息通过延迟队列
配置,如下所示:
package com.lay.rabbitmqdelay.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Description: 消息队列配置信息
* @Author: lay
* @Date: Created in 19:40 2018/12/20
* @Modified By:IntelliJ IDEA
*/
@Configuration
public class RabbitMqConfig {
//消息中心实际消费队列交换配置
@Bean
DirectExchange messageDirectExchange() {
return (DirectExchange) ExchangeBuilder
.directExchange(QueueEnum.MESSAGE_QUEUE.getExchange())
.durable(true)
.build();
}
//消息中心延迟消费交换配置
@Bean
DirectExchange messageDirectTtlExchange() {
return (DirectExchange) ExchangeBuilder
.directExchange(QueueEnum.MESSAGE_TTL_QUEUE.getExchange())
.durable(true)
.build();
}
//消息中心实际消费队列配置
@Bean
Queue messageQueue() {
return new Queue(QueueEnum.MESSAGE_QUEUE.getName());
}
//消息中心Ttl队列配置
@Bean
Queue messageTtlQueue() {
return QueueBuilder
.durable(QueueEnum.MESSAGE_TTL_QUEUE.getName())
.withArgument("x-dead-letter-exchange", QueueEnum.MESSAGE_QUEUE.getExchange())
.withArgument("x-dead-letter-routing-key", QueueEnum.MESSAGE_QUEUE.getRoutingKey())
.build();
}
//消息中心实际消息交换机与队列绑定
@Bean
Binding messageBinding(DirectExchange messageDirectExchange, Queue messageQueue) {
return BindingBuilder
.bind(messageQueue)
.to(messageDirectExchange)
.with(QueueEnum.MESSAGE_QUEUE.getRoutingKey());
}
//消息中心TTL绑定实际TTL绑定延迟消费交换机与队列
@Bean
Binding messageTtlBinding(DirectExchange messageDirectTtlExchange,Queue messageTtlQueue){
return BindingBuilder
.bind(messageTtlQueue)
.to(messageDirectTtlExchange)
.with(QueueEnum.MESSAGE_TTL_QUEUE.getRoutingKey());
}
}
我们声明了消息通知队列
的相关Exchange
、Queue
、Binding
等配置,将message.center.create
队列通过路由键message.center.create
绑定到了message.center.direct
交换上。
除此之外,我们还添加了消息通知延迟队列
的Exchange
、Queue
、Binding
等配置,将message.center.create.ttl
队列通过message.center.create.ttl
路由键绑定到了message.center.topic.ttl
交换上。
我们仔细来看看messageTtlQueue
延迟队列的配置,跟messageQueue
队列配置不同的地方这里多出了x-dead-letter-exchange
、x-dead-letter-routing-key
两个参数,而这两个参数就是配置延迟队列过期后转发的Exchange
、RouteKey
.。
其中,x-dead-letter-exchange声明了队列里的死信转发到的DLX名称。x-dead-letter-routing-key声明了这些死信在转发时携带的routing-key名称。
只要在创建队列时对应添加了这两个参数,在RabbitMQ
管理平台看到的队列配置就不仅是单纯的Direct
类型的队列类型,如下图所示:
队列类型差异
在上图内我们可以看到message.center.create.ttl
队列多出了DLX
、DLK
的配置,这就是RabbitMQ
内死信交换
的标志。
满足死信交换
的条件,在官方文档中表示:
Messages from a queue can be ‘dead-lettered’; that is, republished to another exchange when any of the following events occur:
The message is rejected (basic.reject or basic.nack) with requeue=false,
The TTL for the message expires; or
The queue length limit is exceeded.
- 该消息被拒绝(basic.reject或 basic.nack),requeue = false
- 消息的TTL过期
- 队列长度限制已超出
官方文档地址
我们需要满足上面的其中一种方式就可以了,我们采用满足第二个条件,采用过期的方式。
生产者
MessageProvider.java
package com.lay.rabbitmqdelay.provider;
/**
* @Description:
* @Author: lay
* @Date: Created in 10:09 2018/12/21
* @Modified By:IntelliJ IDEA
*/
@Component
public class MessageProvider {
private static final Logger log= LoggerFactory.getLogger(MessageProvider.class);
@Autowired
private RabbitTemplate rabbitTemplate;
//发送延迟消息
public void sendMessage(Object messageContent,String exchange,String routingKey,final long delayTimes){
if(!StringUtils.isEmpty(exchange)){
log.info("延迟: {}毫秒写入消息队列:{},消息内容:{}",delayTimes,routingKey, JSON.toJSONString(messageContent));
//执行发送消息到指定队列
rabbitTemplate.convertAndSend(exchange,routingKey,messageContent,message -> {
//设置延迟毫秒值
message.getMessageProperties().setExpiration(String.valueOf(delayTimes));
return message;
});
}else{
log.error("未找到队列消息:{},所属的交换机",exchange);
}
}
}
由于我们在 pom.xml
配置文件内添加了RabbitMQ
相关的依赖并且在上面application.yml
文件内添加了对应的配置,SpringBoot
为我们自动实例化了RabbitTemplate
,该实例可以发送任何类型的消息到指定队列。
我们采用convertAndSend
方法,将消息内容发送到指定Exchange
、RouterKey
队列,并且通过setExpiration
方法设置过期时间,单位:毫秒。
消费者
接下来创建一个名为MessageConsumer
的消费者类,该类需要监听消息通知队列
,代码如下所示:
package com.lay.rabbitmqdelay.consumer;
/**
* @Description:
* @Author: lay
* @Date: Created in 10:32 2018/12/21
* @Modified By:IntelliJ IDEA
*/
@Component
@RabbitListener(queues = "message.create")
public class MessageConsumer {
private static final Logger log= LoggerFactory.getLogger(MessageConsumer.class);
@RabbitHandler
public void process(@Payload String content, Message message){
log.info("消费内容:{}",content);
log.info("消费时间:{}",new Date());
log.info("Message内容: {}", JSON.toJSONString(message));
}
}
在@RabbitListener
注解内配置了监听的队列,这里配置内容是QueueEnum
枚举内的queueName
属性值,当然如果你采用常量的方式在注解属性上是直接可以使用的,枚举不支持这种配置,这里只能把QueueName
字符串配置到queues
属性上了。
由于我们在消息发送时采用字符串的形式发送消息内容,这里在@RabbitHandler
处理方法的参数内要保持数据类型一致!
测试
我们在test目录下创建一个测试类,如下所示:
package com.lay.rabbitmqdelay;
/**
* @Description:
* @Author: lay
* @Date: Created in 10:26 2018/12/21
* @Modified By:IntelliJ IDEA
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = RabbitMqDelayApplication.class)
public class RabbitMqDelayApplicationTesst {
@Autowired
private MessageProvider messageProvider;
@Test
public void testDelay(){
messageProvider.sendMessage("测试延迟消费信息,写入时间: "+new Date(),
QueueEnum.MESSAGE_TTL_QUEUE.getExchange(),
QueueEnum.MESSAGE_TTL_QUEUE.getRoutingKey(),
10000);
}
}
问题
第一种方式:经过测试,我们可以发现,当我们先增加一条过期时间大(10000)的A消息进入,之后再增加一个过期时间小的(1000)消息B,并没有出现想象中的B消息先被消费,A消息后被消费,而是出现了当10000过去的时候,AB消息同时被消费,也就是B消息的消费被阻塞了。
为什么会出现这样的现象呢?
我们知道利用TTL DLX特性实现的方式,实际上在第一个延时队列C里面设置了dlx,生产者生产了一条带ttl的消息放入了延时队列C中,等到延时时间到了,延时队列C中的消息变成了死信,根据延时队列C中设置的dlx的exchange的转发规则,转发到了实际消费队列D中,当该队列中的监听器监听到消息时就会正式开始消费。那么实际上延时队列中的消息也是放入队列中的,队列满足先进先出,而延时大的消息A还没出队,所以B消息也不能顺利出队。
解决办法:利用rabbitMQ插件x-delay-message
。
方式二:x-delay-message
为了解决上面的问题,Rabbitmq实现了一个插件x-delay-message
来实现延时队列。
安装
选择rabbitmq_delayed_message_exchange插件,选择3.6版本,进行下载
#将安装包进行解压
uzip rabbitmq_delayed_message_exchange-20171215-3.6.x.zip
#将插件移到rabbitmq安装的路径
cp -r rabbitmq_delayed_message_exchange-20171215-3.6.x.ez /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.15/plugins
#启用插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
配置队列
XdelayConfig.java
package com.lay.rabbitmqdelay.config;
/**
* @Description: 利用x-delayed-message-exchange插件,解决rabbitmq死信队列延时大会阻塞延时小的消息
* @Author: lay
* @Date: Created in 13:16 2018/12/21
* @Modified By:IntelliJ IDEA
*/
@Configuration
public class XdelayConfig {
//队列名
public static final String X_DELAY_MESSAGE_QUEUE="x.delay.message";
public static final String X_DELAY_MESSAGE_EXCHANGE="x.delay.exchange";
public static final String X_DELAY_ROUTING_KEY="x.delay.routing.key";
//创建一个真实消费队列
@Bean
Queue xDelayMessageQueue(){
return new Queue(X_DELAY_MESSAGE_QUEUE,true);
}
@Bean
CustomExchange xDelayExchange(){
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-delayed-type", "direct");
return new CustomExchange(X_DELAY_MESSAGE_EXCHANGE, "x-delayed-message", true, false, args);
}
@Bean
Binding xDelayMessageBinding(Queue xDelayMessageQueue,CustomExchange xDelayExchange){
return BindingBuilder
.bind(xDelayMessageQueue)
.to(xDelayExchange)
.with(X_DELAY_ROUTING_KEY)
.noargs();
}
}
生产者
XdelayProvider.java
package com.lay.rabbitmqdelay.provider;
/**
* @Description:
* @Author: lay
* @Date: Created in 13:32 2018/12/21
* @Modified By:IntelliJ IDEA
*/
@Component
public class XdelayProvider {
private static final Logger log= LoggerFactory.getLogger(XdelayProvider.class);
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendXDelayMessage(Object messageContent,String exchange,String routingKey,final long delayTimes){
if(!StringUtils.isEmpty(exchange)){
log.info("延迟: {} 毫秒写入x-delay消息队列:{},消息内容:{}",delayTimes,routingKey, JSON.toJSONString(messageContent));
//执行发送消息到指定队列
rabbitTemplate.convertAndSend(exchange,routingKey,messageContent,message -> {
//设置延迟毫秒值
message.getMessageProperties().setDelay((int) delayTimes);
return message;
});
}else{
log.error("未找到队列消息:{},所属的交换机",exchange);
}
}
}
消费者
XDelayConsumer.java
package com.lay.rabbitmqdelay.consumer;
/**
* @Description:
* @Author: lay
* @Date: Created in 10:32 2018/12/21
* @Modified By:IntelliJ IDEA
*/
@Component
@RabbitListener(queues = XdelayConfig.X_DELAY_MESSAGE_QUEUE)
public class XDelayConsumer {
private static final Logger log= LoggerFactory.getLogger(XDelayConsumer.class);
@RabbitHandler
public void process(@Payload String content, Message message){
log.info("x-delay消费内容:{}",content);
log.info("x-delay消费时间:{}",new Date());
log.info("Message内容: {}", JSON.toJSONString(message));
}
}
测试类
package com.lay.rabbitmqdelay;
/**
* @Description:
* @Author: lay
* @Date: Created in 10:26 2018/12/21
* @Modified By:IntelliJ IDEA
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = RabbitMqDelayApplication.class)
public class RabbitMqXDelayApplicationTesst {
@Autowired
private XdelayProvider xdelayProvider;
@Test
public void testDelay(){
xdelayProvider.sendXDelayMessage("测试x-delay延迟消费信息,写入时间: "+new Date(),
XdelayConfig.X_DELAY_MESSAGE_EXCHANGE,
XdelayConfig.X_DELAY_ROUTING_KEY,
10000);
}
}