springboot 整合 RabbitMQ
背景
多线程读取大文件后需要分时段进行第三方接口的调用【由于外部接口限制API30s才可以调用一次】,所以我们将使用RabbitMQ的延时消费的方式进行外部接口的调用,以保证项目程序的正常运行
RabbitMQ介绍
RabbitMQ介绍
RabbitMQ 快速下载安装
详情看上 ,本篇文章只解决背景问题,整合springboot+RabbitMQ
springboot 整合 RabbitMQ[延时队列]
延时队列
延时队列是存储延时消息的队列,延时消息就是生产者发送了一条消息,但是不希望该消息不要被立即消费,而是设置一个延时时间,等过了这个时间再消费消息
延时队列安装插件
在RabbitMQ3.6版本以前一般采用死信队列 + TTL过期时间的方式来实现延迟队列。
从3.6以后的版本,RabbitMQ官方提供了延迟对列的插件官方下载地址rabbitmq_delayed_message_exchange-3.8.0.ez 这个插件放到 RabbitMQ 安装目录的 plugins文件中
RabbitMQ 安装 sbin 文件中用 cmd 执行命令:
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
然后登陆web页面,可以查看到交换机tab下新增了x-delayed-message
插件原理

- 原始的 死信队列+ TTL 的模式,消息首先会路由到一个正常的队列,根据设置的 TTL 进入死信队列,与之不同的是通过
x-delayed-message声明的交换机,它的消息在发布之后不会立即进入队列,先将消息保存至 Mnesia(一个分布式数据库管理系统,适合于电信和其它需要持续运行和具备软实时特性的 Erlang 应用) - 这个插件将会尝试确认消息是否过期,首先要确保消息的延迟范围是 Delay > 0, Delay =< ?ERL_MAX_T(在 Erlang 中可以被设置的范围为 (2^32)-1 毫秒),如果消息过期通过
x-delayed-type类型标记的交换机投递至目标队列,整个消息的投递过程也就完成了
代码具体实现
pom.xml
<!--springboot 2.3.1.RELEASE--><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>
application.properties
# 由于MQ只在本项目中,未跨项目 所有配置文件一份,跨项目则需要分开配置spring.rabbitmq.host=127.0.0.1spring.rabbitmq.port=5672spring.rabbitmq.username=guestspring.rabbitmq.password=guest#开启消息的return机制spring.rabbitmq.publisher-confirms=true#确认消息已发送到队列spring.rabbitmq.publisher-returns=true#开启消息的confirm机制 correlated:开启;NONE:关闭spring.rabbitmq.publisher-confirms-type=correlated#在需要使用消息的return机制时候,此参数必须设置为truespring.rabbitmq.template.mandatory=truespring.rabbitmq.listener.type=simple#消费方消息确认:手动确认spring.rabbitmq.listener.simple.acknowledge-mode=manual#决定被拒绝的消息是否重新入队;默认是true(与参数acknowledge-mode有关系)spring.rabbitmq.listener.simple.default-requeue-rejected=false
静态常量抽取
/*** MQ相关常量**/public class MqConstant {// 延时交换机public static final String MQ_WEBSITE_FILM_DELAY_EXCHANGE = "website_film_delay_exchange";// 延时队列名称public static final String MQ_WEBSITE_FILM_QUEUE = "website_film_delay_queue";// 普通交换机public static final String MQ_WEBSITE_NORMAL_EXCHANGE = "website_normal_exchange";// 普通队列名称public static final String MQ_WEBSITE_NORMAL_QUEUE = "website_normal_queue";// 普通交换机路由键public static final String MQ_WEBSITE_NORMAL_ROUTING_KEY = "website_normal_routing_key";}
配置类
RabbitConfig
主要是队列,交换机的配置绑定
import org.springframework.amqp.core.*;import org.springframework.amqp.rabbit.connection.ConnectionFactory;import org.springframework.amqp.rabbit.core.RabbitAdmin;import org.springframework.amqp.rabbit.core.RabbitTemplate;import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import java.util.HashMap;import java.util.Map;/*** RabbitMQ的配置 主要是队列,交换机的配置绑定**/@Configurationpublic class RabbitConfig {//https://blog.csdn.net/weixin_38192427/article/details/120509586/*** durable="true" 持久化 rabbitmq重启的时候不需要创建新的队列* exclusive 表示该消息队列是否只在当前connection生效,默认是false* auto-delete 表示消息队列没有在使用时将被自动删除 默认是false*/// 延时队列@Beanpublic Queue delayQueue() {return new Queue(MqConstant.MQ_WEBSITE_FILM_QUEUE, true,false,false);}/*** 交换机说明:* durable="true" rabbitmq重启的时候不需要创建新的交换机* auto-delete 表示交换机没有在使用时将被自动删除 默认是false* direct交换器相对来说比较简单,匹配规则为:如果路由键匹配,消息就被投送到相关的队列* topic交换器你采用模糊匹配路由键的原则进行转发消息到队列中* fanout交换器中没有路由键的概念,他会把消息发送到所有绑定在此交换器上面的队列中。*/// 延时交换机 设置public FanoutExchange delayExchange() {//使用自定义交换器Map<String, Object> args = new HashMap<>();args.put("x-delayed-type", "direct");FanoutExchange fanoutExchange = new FanoutExchange(MqConstant.MQ_WEBSITE_FILM_DELAY_EXCHANGE, true, false, args);fanoutExchange.setDelayed(true);return fanoutExchange;}// 绑定延时队列与延时交换机@Beanpublic Binding delayBind() {return BindingBuilder.bind(delayQueue()).to(delayExchange());}// ------------------------普通队列------------------------// 普通队列@Beanpublic Queue normalQueue() {return new Queue(MqConstant.MQ_WEBSITE_NORMAL_QUEUE, true);}// 普通交换机@Beanpublic DirectExchange normalExchange() {return new DirectExchange(MqConstant.MQ_WEBSITE_NORMAL_EXCHANGE, true, false);}// 绑定普通消息队列@Beanpublic Binding normalBind() {return BindingBuilder.bind(normalQueue()).to(normalExchange()).with(MqConstant.MQ_WEBSITE_NORMAL_ROUTING_KEY);}// 定义消息转换器@Beanpublic Jackson2JsonMessageConverter jsonMessageConverter() {return new Jackson2JsonMessageConverter();}/** ======================== 定制一些处理策略 =============================*//*** 定制化amqp模版* <p>* Rabbit MQ的消息确认有两种。* <p>* 一种是消息发送确认:这种是用来确认生产者将消息发送给交换机,交换机传递给队列过程中,消息是否成功投递。* 发送确认分两步:一是确认是否到达交换机,二是确认是否到达队列* <p>* 第二种是消费接收确认:这种是确认消费者是否成功消费了队列中的消息。*/// 定义消息模板用于发布消息,并且设置其消息转换器@Beanpublic RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);// 消息发送失败返回到队列中, yml需要配置 publisher-returns: truerabbitTemplate.setMandatory(true);/*** 使用该功能需要开启消息确认,yml需要配置 publisher-confirms: true* 通过实现ConfirmCallBack接口,用于实现消息发送到交换机Exchange后接收ack回调* correlationData 消息唯一标志* ack 确认结果* cause 失败原因*/rabbitTemplate.setConfirmCallback(new MsgSendConfirmCallback());/*** 使用该功能需要开启消息返回确认,yml需要配置 publisher-returns: true* 通过实现ReturnCallback接口,如果消息从交换机发送到对应队列失败时触发* message 消息主体 message* replyCode 消息主体 message* replyText 描述* exchange 消息使用的交换机* routingKey 消息使用的路由键*/rabbitTemplate.setReturnCallback(new MsgSendReturnCallback());rabbitTemplate.setMessageConverter(jsonMessageConverter());return rabbitTemplate;}// --------------------------使用RabbitAdmin启动服务便创建交换机和队列--------------------------@Beanpublic RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);// 只有设置为 true,spring 才会加载 RabbitAdmin 这个类rabbitAdmin.setAutoStartup(true);// 创建延时交换机和对列rabbitAdmin.declareExchange(delayExchange());rabbitAdmin.declareQueue(delayQueue());// 创建普通交换机和对列rabbitAdmin.declareExchange(normalExchange());rabbitAdmin.declareQueue(normalQueue());return new RabbitAdmin(connectionFactory);}}
RabbitConfirmConfig
RabbitMQ的配置 消息发送到 exchange,queue 的回调函数
import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;import org.springframework.amqp.rabbit.connection.ConnectionFactory;import org.springframework.amqp.rabbit.core.RabbitTemplate;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;/*** RabbitMQ的配置 消息发送到 exchange,queue 的回调函数**/@Configurationpublic class RabbitConfirmConfig {private static Logger logger = LogManager.getLogger(RabbitConfirmConfig.class);/** ======================== 定制一些处理策略 =============================*//*** 定制化amqp模版* <p>* Rabbit MQ的消息确认有两种。* <p>* 一种是消息发送确认:这种是用来确认生产者将消息发送给交换机,交换机传递给队列过程中,消息是否成功投递。* 发送确认分两步:一是确认是否到达交换机,二是确认是否到达队列* <p>* 第二种是消费接收确认:这种是确认消费者是否成功消费了队列中的消息。* Springboot中使用ConfirmCallback和ReturnCallback* 注意:* 在需要使用消息的return机制时候,mandatory参数必须设置为true* 新版本开启消息的confirm配置publisher-confirms已经过时,改为使用publisher-confirm-type参数设置(correlated:开启;NONE:关闭)*/@Beanpublic RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {RabbitTemplate rabbitTemplate = new RabbitTemplate();rabbitTemplate.setConnectionFactory(connectionFactory);// 设置开启Mandatory才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数rabbitTemplate.setMandatory(true);/*** 使用该功能需要开启消息确认 无论成功与否,需要配置 publisher-confirms: true* 通过实现ConfirmCallBack接口,用于实现消息发送到交换机Exchange后接收ack回调* correlationData 消息唯一标志* ack 确认结果* cause 失败原因*/rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {logger.info("ConfirmCallback:" + "相关数据:" + correlationData);logger.info("ConfirmCallback:" + "确认情况:" + ack);logger.info("ConfirmCallback:" + "原因:" + cause);});/*** 消息从Exchange路由到Queue失败的回调* 使用该功能需要开启消息返回确认,需要配置 publisher-returns: true* 通过实现ReturnCallback接口,如果消息从交换机发送到对应队列失败时触发* message 消息主体 message* replyCode 消息主体 message* replyText 描述* exchange 消息使用的交换机* routingKey 消息使用的路由键*/rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {logger.info("ReturnCallback:" + "消息:" + message);logger.info("ReturnCallback:" + "回应码:" + replyCode);logger.info("ReturnCallback:" + "回应信息:" + replyText);logger.info("ReturnCallback:" + "交换机:" + exchange);logger.info("ReturnCallback:" + "路由键:" + routingKey);});return rabbitTemplate;}}
MsgSendReturnCallback
消息从交换机发送到对应队列失败时触发
import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;import org.springframework.amqp.core.Message;import org.springframework.amqp.rabbit.core.RabbitTemplate;import org.springframework.stereotype.Component;@Componentpublic class MsgSendReturnCallback implements RabbitTemplate.ReturnCallback {private static Logger logger = LogManager.getLogger(MsgSendReturnCallback.class);/*** 使用该功能需要开启消息返回确认,yml需要配置 publisher-returns: true* message 消息主体 message* replyCode 消息主体 message* replyText 描述* exchange 消息使用的交换机* routingKey 消息使用的路由键* <p>* PS:通过实现ReturnCallback接口,如果消息从交换机发送到对应队列失败时触发* </p>*/@Overridepublic void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {if (exchange.equals(MqConstant.MQ_WEBSITE_FILM_DELAY_EXCHANGE)) {// 如果配置了发送回调ReturnCallback,rabbitmq_delayed_message_exchange插件会回调该方法,因为发送方确实没有投递到队列上,只是在交换器上暂存,等过期/时间到了才会发往队列。// 所以如果是延迟队列的交换器,则直接放过,并不是bugreturn;}String correlationId = message.getMessageProperties().getCorrelationId();logger.debug("消息:{} 发送失败, 应答码:{} 原因:{} 交换机: {} 路由键: {}", correlationId, replyCode, replyText, exchange, routingKey);}}
AckListener
Consumer ACK机制
import com.rabbitmq.client.Channel;import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;import org.springframework.amqp.core.Message;import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;import org.springframework.stereotype.Component;import java.util.Map;/*** Consumer ACK机制:* 1. 设置手动签收。acknowledge="manual"* 2. 让监听器类实现ChannelAwareMessageListener接口* 3. 如果消息成功处理,则调用channel的 basicAck()签收* 4. 如果消息处理失败,则调用channel的basicNack()拒绝签收,broker重新发送给consumer**/@Componentpublic class AckListener implements ChannelAwareMessageListener {private static Logger logger = LogManager.getLogger(AckListener.class);Map<Long,Integer> map;@Overridepublic void onMessage(Message message, Channel channel) throws Exception {long deliveryTag = message.getMessageProperties().getDeliveryTag();try {//1.接收转换消息logger.info("MQ接收转换消息[{}]",new String(message.getBody()));//2. 处理业务逻辑logger.info("处理业务逻辑...");int i = 3/0;//出现错误//3. 手动签收channel.basicAck(deliveryTag,false);} catch (Exception e) {//e.printStackTrace();logger.error("MQ拒绝签收逻辑...");/*4.拒绝签收第三个参数:requeue:重回队列。如果设置为true,则消息重新回到queue,broker会重新发送该消息给消费端*//*if(!map.containsKey(deliveryTag)){map.put(deliveryTag,1);channel.basicNack(deliveryTag,true,true);}else{//判断是否大于3次if(>3){channel.basicNack(deliveryTag,false,true);//记录}else{channel.basicNack(deliveryTag,true,true);}}*/channel.basicNack(deliveryTag,false,false);//channel.basicReject(deliveryTag,true);}}}
MsgProductionService
发送消息到MQ 业务类 消费生产
import org.springframework.amqp.core.MessageDeliveryMode;import org.springframework.amqp.rabbit.core.RabbitTemplate;import org.springframework.stereotype.Service;import javax.annotation.Resource;/*** 发送消息到MQ**/@Service("msgProductionService")public class MsgProductionService {@Resourceprivate RabbitTemplate rabbitTemplate;// 发送延时信息public void sendTimeoutMsg(String exchange,String content, String routingKey, int delay) {// 通过广播模式发布延时消息,会广播至每个绑定此交换机的队列,这里的路由键没有实质性作用rabbitTemplate.convertAndSend(exchange, routingKey, content, message -> {message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);// 毫秒为单位,指定此消息的延时时长message.getMessageProperties().setDelay(delay * 1000);return message;});}// 发送普通消息public void sendMsg(String exchange,String routingKey, String content) {// DirectExchange类型的交换机,必须指定对应的路由键rabbitTemplate.convertAndSend(exchange, routingKey, content);}}
MsgComsumerService
MQ消费 消费者【小编这里是在同一个项目中实现的,生产和消费】
import com.rabbitmq.client.Channel;import com.zy.website.service.FilmService;import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;import org.springframework.amqp.core.Message;import org.springframework.amqp.rabbit.annotation.RabbitHandler;import org.springframework.amqp.rabbit.annotation.RabbitListener;import org.springframework.stereotype.Service;import javax.annotation.Resource;import java.io.IOException;/*** MQ 消费者**/@Service("msgComsumerService")public class MsgComsumerService {private static Logger logger = LogManager.getLogger(MsgComsumerService.class);@ResourceFilmService filmService;//自己业务具体实现类注入// 监听消费延时消息@RabbitListener(queues = {"website_film_delay_queue"})@RabbitHandlerpublic void process(String content, Message message, Channel channel) throws IOException {try {// 消息的可定确认,第二个参数如果为true将一次性确认所有小于deliveryTag的消息channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);//调用方法消费消息 自己业务具体实现类filmService.getFilmInfoByExternalApi(content);logger.info("延迟队列消息[{}]被消费!!",content);} catch (Exception e) {logger.error("延迟队列消息 处理失败:{}", e.getMessage());// 直接拒绝消费该消息,后面的参数一定要是false,否则会重新进入业务队列,不会进入死信队列channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);/*// ack返回false,requeue-true并重新回到队列channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);*/}}// 消费普通消息@RabbitListener(queues = {"website_normal_queue"})@RabbitHandlerpublic void process1(String content, Message message, Channel channel) throws IOException {try {logger.info("普通队列的内容[{}]", content);// 消息的可定确认,第二个参数如果为true将一次性确认所有小于deliveryTag的消息channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);logger.info("普通信息处理完毕");} catch (Exception e) {logger.error("处理失败:{}", e.getMessage());// 直接拒绝消费该消息,后面的参数一定要是false,否则会重新进入业务队列,不会进入死信队列channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);}}}
**代码实现完成,这只是简单实现,复杂还需要研究。。。。。**
RabbitMQ的具体详细配置项
基础配置项
spring.rabbitmq.host: 默认localhostspring.rabbitmq.port: 默认5672spring.rabbitmq.username: 用户名spring.rabbitmq.password: 密码spring.rabbitmq.virtual-host: 连接到代理时用的虚拟主机spring.rabbitmq.addresses: 连接到server的地址列表(以逗号分隔),先addresses后hostspring.rabbitmq.requested-heartbeat: 请求心跳超时时间,0为不指定,如果不指定时间单位默认为妙spring.rabbitmq.publisher-confirms: 是否启用【发布确认】,默认falsespring.rabbitmq.publisher-returns: 是否启用【发布返回】,默认falsespring.rabbitmq.connection-timeout: 连接超时时间,单位毫秒,0表示永不超时
SSL
spring.rabbitmq.ssl.enabled: 是否支持ssl,默认falsespring.rabbitmq.ssl.key-store: 持有SSL certificate的key store的路径spring.rabbitmq.ssl.key-store-password: 访问key store的密码spring.rabbitmq.ssl.trust-store: 持有SSL certificates的Trust storespring.rabbitmq.ssl.trust-store-password: 访问trust store的密码spring.rabbitmq.ssl.trust-store-type=JKS:Trust store 类型.spring.rabbitmq.ssl.algorithm: ssl使用的算法,默认由rabiitClient配置spring.rabbitmq.ssl.validate-server-certificate=true:是否启用服务端证书验证spring.rabbitmq.ssl.verify-hostname=true 是否启用主机验证
缓存cache
spring.rabbitmq.cache.channel.size: 缓存中保持的channel数量spring.rabbitmq.cache.channel.checkout-timeout: 当缓存数量被设置时,从缓存中获取一个channel的超时时间,单位毫秒;如果为0,则总是创建一个新channelspring.rabbitmq.cache.connection.size: 缓存的channel数,只有是CONNECTION模式时生效spring.rabbitmq.cache.connection.mode=channel: 连接工厂缓存模式:channel 和 connection
Listener
spring.rabbitmq.listener.type=simple: 容器类型.simple或directspring.rabbitmq.listener.simple.auto-startup=true: 是否启动时自动启动容器spring.rabbitmq.listener.simple.acknowledge-mode: 表示消息确认方式,其有三种配置方式,分别是none、manual和auto;默认autospring.rabbitmq.listener.simple.concurrency: 最小的消费者数量spring.rabbitmq.listener.simple.max-concurrency: 最大的消费者数量spring.rabbitmq.listener.simple.prefetch: 一个消费者最多可处理的nack消息数量,如果有事务的话,必须大于等于transaction数量.spring.rabbitmq.listener.simple.transaction-size: 当ack模式为auto时,一个事务(ack间)处理的消息数量,最好是小于等于prefetch的数量.若大于prefetch, 则prefetch将增加到这个值spring.rabbitmq.listener.simple.default-requeue-rejected: 决定被拒绝的消息是否重新入队;默认是true(与参数acknowledge-mode有关系)spring.rabbitmq.listener.simple.missing-queues-fatal=true 若容器声明的队列在代理上不可用,是否失败; 或者运行时一个多多个队列被删除,是否停止容器spring.rabbitmq.listener.simple.idle-event-interval: 发布空闲容器的时间间隔,单位毫秒spring.rabbitmq.listener.simple.retry.enabled=false: 监听重试是否可用spring.rabbitmq.listener.simple.retry.max-attempts=3: 最大重试次数spring.rabbitmq.listener.simple.retry.max-interval=10000ms: 最大重试时间间隔spring.rabbitmq.listener.simple.retry.initial-interval=1000ms:第一次和第二次尝试传递消息的时间间隔spring.rabbitmq.listener.simple.retry.multiplier=1: 应用于上一重试间隔的乘数spring.rabbitmq.listener.simple.retry.stateless=true: 重试时有状态or无状态spring.rabbitmq.listener.direct.acknowledge-mode= ack模式spring.rabbitmq.listener.direct.auto-startup=true 是否在启动时自动启动容器spring.rabbitmq.listener.direct.consumers-per-queue= 每个队列消费者数量.spring.rabbitmq.listener.direct.default-requeue-rejected= 默认是否将拒绝传送的消息重新入队.spring.rabbitmq.listener.direct.idle-event-interval= 空闲容器事件发布时间间隔.spring.rabbitmq.listener.direct.missing-queues-fatal=false若容器声明的队列在代理上不可用,是否失败.spring.rabbitmq.listener.direct.prefetch= 每个消费者可最大处理的nack消息数量.spring.rabbitmq.listener.direct.retry.enabled=false 是否启用发布重试机制.spring.rabbitmq.listener.direct.retry.initial-interval=1000ms # Duration between the first and second attempt to deliver a message.spring.rabbitmq.listener.direct.retry.max-attempts=3 # Maximum number of attempts to deliver a message.spring.rabbitmq.listener.direct.retry.max-interval=10000ms # Maximum duration between attempts.spring.rabbitmq.listener.direct.retry.multiplier=1 # Multiplier to apply to the previous retry interval.spring.rabbitmq.listener.direct.retry.stateless=true # Whether retries are stateless or stateful.
Template
spring.rabbitmq.template.mandatory: 启用强制信息;默认falsespring.rabbitmq.template.receive-timeout: receive() 操作的超时时间spring.rabbitmq.template.reply-timeout: sendAndReceive() 操作的超时时间spring.rabbitmq.template.retry.enabled=false: 发送重试是否可用spring.rabbitmq.template.retry.max-attempts=3: 最大重试次数spring.rabbitmq.template.retry.initial-interva=1000msl: 第一次和第二次尝试发布或传递消息之间的间隔spring.rabbitmq.template.retry.multiplier=1: 应用于上一重试间隔的乘数spring.rabbitmq.template.retry.max-interval=10000: 最大重试时间间隔
