Spring Reactive Web Webflux 整合 rabbitMQ
引言
在使用spring-web 的 websocket 时
我们可以在@RabbitListener或CloudStream @StreamListener中直接使用messagingTemplate.convertAndSend或@SendTo 广播消息。只需要一个消费者监听就可以,不管有没有客户端连接。
在webflux中如何使用mq进行消息推送呢?
首先需要知道的是,当用户请求发生时,我们才创建队列,因为这和websocket不同,我们是被拉取方,我们无法主动发送消息,SSE协议本身就是如此。
因此我们需要针对每个用户创建一个单独的队列,否则就成轮询或公平分发了。
下面不写废话,不写原理,不复制粘贴源码美其名曰分析,直接上例子。
例子(广播消息)
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
Configuration
创建扇形交换机和RabbitAdmin,这里不创建绑定,因为用户请求时,才会创建队列和绑定,然后监听消息。
CachingConnectionFactory 是amqp的autoconfiguration自动配置的,写接口名也可以,因为代码中没有import具体包名,怕你们导入错,所以使用的CachingConnectionFactory
创建扇形交换机的原因就是为了广播,fanout不关心路由键,和此交换机绑定的所有队列都可以收到消息,有些人喜欢topic,用topic也行。
@Configuration
public class RabbitMqConfig {
@Bean
Exchange fanoutExchange(){
return ExchangeBuilder.fanoutExchange("fanoutExchange").durable(true).build();
}
@Bean
public RabbitAdmin rabbitAdmin(CachingConnectionFactory connectionFactory){
return new RabbitAdmin(connectionFactory);
}
}
写一个监听容器工厂
功能就是当用户请求时,使用工厂创建队列和进行绑定,并返回监听容器。
写了2个create,一个是可以指定队列名,一个是随机队列名,如果需要根据用户名进行队列创建可以使用第一个。
容器中的队列可以多个,有需要可以自己改一改
注意:创建容器时,我只放入了队列,并没有设置Listener,设置Listener是在用户请求创建时才会设置。因为需要在Listner代码块中使用Flux的sink去推送消息,所以无法提前创建。
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class MessageListenerContainerFactory {
@Autowired
private CachingConnectionFactory connectionFactory;
@Autowired
private RabbitAdmin rabbitAdmin;
@Autowired
private Exchange fanoutExchange;
public SimpleMessageListenerContainer create(String queueName) {
Queue queue = QueueBuilder.nonDurable(queueName).maxLength(10000).autoDelete().exclusive().build();
rabbitAdmin.declareQueue(queue);
rabbitAdmin.declareBinding(BindingBuilder.bind(queue).to(fanoutExchange).with("").noargs());
SimpleMessageListenerContainer simpleMessageListenerContainer = new SimpleMessageListenerContainer();
//在容器中放入刚创建好的队列
simpleMessageListenerContainer.setQueueNames(queue.getName());
simpleMessageListenerContainer.setConnectionFactory(connectionFactory);
/*
//设置当前的消费者数量
simpleMessageListenerContainer.setConcurrentConsumers(1);
simpleMessageListenerContainer.setMaxConcurrentConsumers(1);
//设置消息是否重回队列
simpleMessageListenerContainer.setDefaultRequeueRejected(false);
//设置自动确认消息simpleMessageListenerContainer.setAcknowledgeMode(AcknowledgeMode.AUTO);
//设置暴露监听器通道
simpleMessageListenerContainer.setExposeListenerChannel(true);
*/
return simpleMessageListenerContainer;
}
public SimpleMessageListenerContainer create(){
Queue queue = QueueBuilder.nonDurable().maxLength(10000).autoDelete().exclusive().build();
rabbitAdmin.declareQueue(queue);
rabbitAdmin.declareBinding(BindingBuilder.bind(queue).to(fanoutExchange).with("").noargs());
SimpleMessageListenerContainer simpleMessageListenerContainer = new SimpleMessageListenerContainer();
//在容器中放入刚创建好的队列
simpleMessageListenerContainer.setQueueNames(queue.getName());
simpleMessageListenerContainer.setConnectionFactory(connectionFactory);
return simpleMessageListenerContainer;
}
}
在Controller中使用
//我们自己写的工厂
@Autowired
MessageListenerContainerFactory containerFactory;
@RequestMapping(value = "/test",produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@ResponseBody
public Flux<String> test() {
//用自己写的工厂创建一个监听容器
SimpleMessageListenerContainer container = containerFactory.create();
return Flux.create(sink->{
//容器中设置监听器用于接收到消息后使用sink发送给客户端
container.setupMessageListener((ChannelAwareMessageListener)(Message message, Channel channel)->{
if (sink.isCancelled()) {
container.stop();
return;
}
String msg = new String(message.getBody());
sink.next(msg);
});
//启动容器和停止容器
sink.onRequest(r -> container.start());
sink.onDispose(container::stop);
});
}
使用rabbitmq和redis的区别是,队列名无法固定,必须一个请求对应一个队列,否则消息会成轮询或公平分发(手动ack),因此在监听容器中,队列名要么根据用户名创建,要么不写队列名会随机生成。
思考
如果基于SSE协议,难道只能每个请求创建一个队列吗?10w客户端创建10w个队列?有没有办法优化呢?
首先,上面提到过,如果相同队列会产生轮询或公平分发,但是针对每个请求创建一个队列太过浪费,事实上我们可以这么做:
- 每个服务创建一个队列,这样达到广播的目的
- 在这个队列监听者中,我们进行消息消费,将消息转发到我们自己写被观察者类中。
- 客户端请求,我们添加为观察者,如果被观察者有变化,推送消息。
这样我们就可以做到每个服务一个队列,所有消费我们自己通知。