场景
springboot项目整合rabbitmq后,在项目启动后要先去加载一些文件作为缓存,比如加载千万ID去重(详见https://blog.youkuaiyun.com/qq_41578037/article/details/137463619),但因为项目rabbitmq消费者是用的@RabbitListener,最后发现在项目启动时,@RabbitListener和缓存的加载顺序不分先后,导致可能因为缓存没有加载完成,但是消息已经消费掉了。
要解决的问题
在@RabbitListener消费者开始消费消息前,保证缓存已经全部加载完毕。
好了,其实问题就在于加载顺序这块,网上也有一些rabbitmq懒加载的说明,自己试了试,发现很多文章都是只说了部分,导致自己加到项目中发现不生效,唉,还是自己多试试吧。。。。
项目pom
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置文件
rabbitmq的连接配置
rabbitmq:
virtual-host: test
host: 127.0.0.1
port: 5672
username: guest
password: guest
template:
mandatory: true
listener:
simple:
acknowledge-mode: manual
prefetch: 10
项目启动实现CommandLineRunner
import com.test.GlobalCache;
import com.test.util.FileUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* 初始化操作
*/
@Component
public class TestStartOverRun implements CommandLineRunner {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private ApplicationContext context;
@Autowired
private RabbitListenerEndpointRegistry endpointRegistry;
@Override
public void run(String... args) throws Exception {
//项目启动后要做的事情...
readFiles();
logger.info("task start over something over");
//启动rabbitmq消费者
endpointRegistry.start();
}
private void readFiles(){
logger.info("启动后去读取文件");
}
}
在CommandLineRunner 中注入RabbitListenerEndpointRegistry ,并且在自己项目启动完成,然后做完要做的事情后,调用 endpointRegistry.start();启动消费者。
RabbitmqConfig配置
@Component
public class RabbitConfig {
@Autowired
ConnectionFactory rabbitConnectionFactory;
//这个是rabbitmq批量消费
@Bean("batchRabbitContainerFactory")
public SimpleRabbitListenerContainerFactory batchQueueRabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
//设置批量
factory.setBatchListener(true);
factory.setConsumerBatchEnabled(true);
factory.setBatchSize(20);
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
//重点!!!
factory.setAutoStartup(false);
return factory;
}
//这个是默认单条消费
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
//重点!!!
factory.setAutoStartup(false);
return factory;
}
}
这一步主要就是为了factory.setAutoStartup(false);关闭消费者的自动启动。
消费者
@Component
public class TestListener {
//正常配置,之前怎么配置就怎么配置
@RabbitListener(queues = "consumer_queue", concurrency = "1-5")
public void testHandler(Channel channel, Message msg) {
String content = new String(msg.getBody());
}
}
效果
直接重启项目,可以发现只有在调用endpointRegistry.start();后,定义的消费者才开始消费消息,在rabbitmq控制台中,也可以看到,项目启动后,在队列的消费者那里刚开始是没有消费者信息的。
至此,可以说效果上已经达到了我们刚开始想要的效果。
背后原理
我们可以看下endpointRegistry.start();方法究竟是怎么做的。
@Override
public void start() {
for (MessageListenerContainer listenerContainer : getListenerContainers()) {
startIfNecessary(listenerContainer);
}
}
private void startIfNecessary(MessageListenerContainer listenerContainer) {
if (this.contextRefreshed || listenerContainer.isAutoStartup()) {
listenerContainer.start();
}
}
其实就是两个步骤,遍历MessageListenerContainer ,然后启动。
在启动的时候还判断this.contextRefreshed || listenerContainer.isAutoStartup()为true才开始启动,可以看到这是两个条件!!!
contextRefreshed 为true还是false呢?
private boolean contextRefreshed;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (event.getApplicationContext().equals(this.applicationContext)) {
this.contextRefreshed = true;
}
}
可以看到这个值默认是false的,只有在ContextRefreshedEvent 事件后才被设置为true。
那listenerContainer.isAutoStartup()默认是true还是false呢?
default boolean isAutoStartup() {
return true;
}
isAutoStartup默认是true,也就是rabbitmq config中我们要设置autoStartUp为false的原因!!!
等于默认如果不设置autoStartUp为false的话,它是true,所以项目启动后this.contextRefreshed || listenerContainer.isAutoStartup() 这个条件为true,然后消费者启动消费,从而和我们要提前做的事情顺序混乱。
延伸
至此,我们已经知道了前因后果,找到了rabbitmq消费者启动消费的时机,我们其实有多种方式去实现消费者的懒启动!!!
可以重写ContextRefreshedEvent事件,等我们要做的事情做完,手动去发一个事件!当然这样做的前提是要把AutoStartup设置为false!!!