使用RabbitMQ实现消息的延迟消费
最近做的项目涉及到后台消息的推送和app端接受消息的功能。具体的要求是:后台向app用户推送的消息,app用户能在app里面的消息栏目查看到消息详情,后台推送的时候能选择定时推送和立即推送两种方案。由于公司的基础架构以及公司文化原因,我们选择了RabbitMQ来实现推送服务和消息盒子之间的通讯。
注意:由于知识有限,本篇文章只对RabbitMQ做基础的介绍,使用场景,以及代码实现,不会对它的高级用法和策略进行介绍。
RabbitMQ是有erlang语言开发,基于AMQP(Advanced Message Queue 高级消息队列协议)协议实现的消息队列。
常见的消息队列有:RabbitMQ、Kafka 和 ActiveMQ
RabbitMQ最初起源于金融系统,用于不同模块之间的消息通讯。
优点:
1.生产者(Producer):消息的制造者
2.消费者(Consumer):消息的消费者
3.消息(Message):消息对象,包括业务参数和mq的参数
4.队列(Queue):缓存或暂存储消息的容器
5.连接(Connection):应用服务和mq进行交互的tcp连接
6.信道(Channel):AMQP命令都是通过信道来完成的,它是一个虚拟的通道,来复用ctp连接。
7.交换机(Exchange):负责从生产者接收消息,根据路由规则发送到指定的队列里面。
8.路由键(Routing Key):交换机根据路由键将消息发往指定的队列。
9.虚拟主机(Virtual Host):就像是一个RabbitMQ 服务。
一张图介绍RabbitMQ组成以及各个组件之间的关系(参考网上画的)。
交换机是用来发送消息到指定的队列里面的,它使用哪种路由算法是由交换机类型和绑定的规则所决定的。
直连交换机是根据交换机和队列之间绑定的路由键,来将消息发往指定的队列里面。如果交换机与多个队列绑定,则在发送携带路由键的消息时,只发给此路由键的队列,每个队列都是相同副本(比较适合一对一)。
例如:我用直连交换机test-direct-exchange根据路由键test-direct发送一条消息,然后去队列里面看消息,如图所示
分割线------------------------------------------------------------------------------分割线
扇形交换机是将消息发往与它绑定的队列,而不去理会绑定的路由键是否一致。如果交换机与多个队列绑定,每个队列都是相同副本,起到广播的作用。
例如:我用扇形交换机test-fanout-exchange根据路由键test-fanout发送一条消息,然后去队列里面看消息,如图所示
分割线------------------------------------------------------------------------------分割线
但是三个队列都收到了消息,可见扇形交换机会忽略其路由键
主题交换机是通过消息的路由键跟交换机和队列之间的绑定路由键进行匹配,将消息发给匹配上的队列,跟直连交换机的一对多相似,但是他的路由键可以支持模糊匹配。
例如:我用主题交换机test-topic-exchange根据路由键test.topic2发送一条消息,然后去队列里面看消息,如图所示
分割线------------------------------------------------------------------------------分割线
根据消息路由键和绑定的路由键进行模糊匹配,推送消息。
头交换机是主题交换机有点相似,主题交换机是基于路由键,而头交换机是基于消息的headers数据,所以在发送消息给头交换机时指定Routing key是不起作用的。头交换机在绑定队列时需要指定参数Arguments,发送消息时需要指定headers和Arguments相匹配,消息才能被推到相应的队列。
例如:我用头交换机test-headers-exchange根据路由键test-headers1发送一条消息,然后去队列里面看消息,如图所示
分割线------------------------------------------------------------------------------分割线
如果前两个队列能收到消息,证明路由键不生效。
定时推送,顾名思义就是要让消息的发送时间给延迟,要想发送延迟消息,就要用到延迟队列。RabbitMQ没有提供延迟队列,其有自带的死信队列和消息存活时间。
对于此需求有两种解决方案:一、使用插件,延迟队列来实现。二、使用消息存活时间和死信队列来实现。我是基于第二种方法来实现的。
RabbitMQ可以根据队列或消息不同颗粒度来设置消息的生存时间,如果同时设置,则会根据最早的到期时间为准,将消息变为dead letter(死信)。
显而易见,针对消息设置生存时间更加灵活,我也是选用的这种方式。
RabbitMQ可以在队列里配置死信交换机然后通过路由键绑定到另一个队列上,如果该队列里面的消息dead letter,就会通过交换机发往另一个队列里面,然后就可以消费了。
队列里面出现dead letter的情况有以下几种:
实现消息延迟发送的具体思路是:首先创建一个交换机来当做死信交换机,再创建一个队列与这个死信交换机进行绑定就称作死信队列。其次创建一个交换机来当做正常交换机,在创建一个队列与这个正常交换机进行绑定,同时将死信交换机和死信路由键配置到这个正常队列里面。这样,当一条带有存活时间的消息通过正常交换机发送过来时,首先进入正常队列里面,然后到了存活时间,就会通过死信交换机根据路由键发送到死信队列里面,然后消费者消费死信队列里的消息,就达到了延迟消费的目的。
java代码实现。
1.首先创建maven项目,导入pom文件
<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.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>
</plugin>
</plugins>
</build>
2.配置配置文件(如果是yml,就用对应的书写规则)
spring.rabbitmq.host=localhost
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/
3.配置mq的相关组件
package com.wps.cn.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @author wangps
* @date 2022年11月22日 14:44
*/
@Configuration
public class QueueConfig {
public static final String NORMAL_QUEUE_NAME = "normal_queue_name";
public static final String NORMAL_EXCHANGE_NAME = "normal_exchange_name";
public static final String NORMAL_ROUTING_KEY = "normal_routing_key";
public static final String DLX_QUEUE_NAME = "dlx_queue_name";
public static final String DLX_EXCHANGE_NAME = "dlx_exchange_name";
public static final String DLX_ROUTING_KEY = "dlx_routing_key";
/**
* 死信队列
* @return
*/
@Bean
Queue dlxQueue() {
return new Queue(DLX_QUEUE_NAME, true);
}
/**
* 死信交换机
* @return
*/
@Bean
DirectExchange dlxExchange() {
return new DirectExchange(DLX_EXCHANGE_NAME);
}
/**
* 绑定死信队列和死信交换机
* @return
*/
@Bean
Binding dlxBinding() {
return BindingBuilder.bind(dlxQueue()).to(dlxExchange())
.with(DLX_ROUTING_KEY);
}
/**
* 普通消息队列
* @return
*/
@Bean
Queue normalQueue() {
Map<String, Object> args = new HashMap<>();
//设置消息过期时间,此方法是在队列的颗粒度设置,比较局限,所以在消息上设置过期时间
// args.put("x-message-ttl", 1000*5);
//设置死信交换机
args.put("x-dead-letter-exchange", DLX_EXCHANGE_NAME);
//设置死信 routing_key
args.put("x-dead-letter-routing-key", DLX_ROUTING_KEY);
return new Queue(NORMAL_QUEUE_NAME, true, false, false, args);
}
/**
* 普通交换机
* @return
*/
@Bean
DirectExchange normalExchange() {
return new DirectExchange(NORMAL_EXCHANGE_NAME);
}
/**
* 绑定普通队列和与之对应的交换机
* @return
*/
@Bean
Binding nomalBinding() {
return BindingBuilder.bind(normalQueue())
.to(normalExchange())
.with(NORMAL_ROUTING_KEY);
}
}
4.创建消费者
package com.wps.cn.consumer;
import com.rabbitmq.client.Channel;
import com.wps.cn.config.QueueConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* @author wangps
* @date 2022年11月22日 14:55
*/
@Component
public class DlxConsumer {
private static final Logger logger = LoggerFactory.getLogger(DlxConsumer.class);
@RabbitListener(queues = QueueConfig.DLX_QUEUE_NAME)
public void process(String order, Message message, @Headers Map<String, Object> headers, Channel channel) {
logger.info("订单号消息", order);
System.out.println("执行结束...."+message);
}
}
5.创建controller当做生产者
package com.wps.cn.controller;
import com.wps.cn.config.QueueConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
/**
* @author wangps
* @date 2022年11月22日 15:50
*/
@RestController
@RequestMapping("/producer")
public class TestProducer {
private static final Logger logger = LoggerFactory.getLogger(TestProducer.class);
@Autowired
private AmqpTemplate rabbitTemplate;
@GetMapping("/sendMessage")
public Object submit(){
String orderId = UUID.randomUUID().toString();
logger.info("提交订单消息========",orderId);
rabbitTemplate.convertAndSend(
QueueConfig.NORMAL_EXCHANGE_NAME,QueueConfig.NORMAL_ROUTING_KEY,
orderId,
message -> {
message.getMessageProperties().setExpiration(1000*5+"");
return message;
});
return "{'orderId':'"+orderId+"'}";
}
}
6.创建启动类
package com.wps.cn;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DxlRabbitmqTestApplication {
public static void main(String[] args) {
SpringApplication.run(DxlRabbitmqTestApplication.class, args);
}
}
7.运行启动类,然后在页面访问,模拟推送消息
http://localhost:8080/producer/sendMessage
观察日志可以看出,消息发出后,在5s后消费者收到消息,从而达到延迟消费的情况。
以上就是今天所讲的RabbitMQ实现延迟消费,本文主要使用了RabbitMQ的TTL和DLX来实现的。虽然说也是看了网上前辈们的总结,但是也有自己的实操经验,也是为了记录下来能够自己加深印象。
如果有不对的地方还请赐教,希望此篇文章能够对你有所帮助。