最近由于工作需要接触了RabbitMQ,并简单的进行了尝试。所以记录一下学习到的相关知识。
在一切开始之前,我们先看一下为什么需要使用消息队列?
https://blog.csdn.net/songfeihu0810232/article/details/78648706
http://www.cnblogs.com/xuyatao/p/6864109.html
RabbitMQ
首先我们自然是从RabbitMQ开始入手,而rabbitmq官网给了我们很好的教程——开始6步。
这里大概阐述一下这六步分别讲述了哪些内容,具体代码示例可在我上面的GitHub链接中看到,或者直接看官网中的代码,基本相同。
第一步
简单示例,大概熟悉生产者向队列发送消息,消费者从队列接收消息。
第二步
多个消费者时,使用轮询的形式消费message;
但是如果某个消费者挂掉,会丢失信息,需要通过设置ack来实现存在consumer的时候就不会丢失message;
上述是在consumer挂掉时不丢失数据,但是如果server挂掉,数据还是会丢失,通过设置server端两个位置可以保证server挂了,数据也不会丢失,分别为如下两个位置:
channel.queueDeclare(TASK_QUEUE_NAME, durable, false, false, null); //其中durable值为true;
channel.basicPublish("", TASK_QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes()); // 设置MessageProperties的值;
注意:rabbitMQ不允许同一名字的队列设置不同声明,所以如果策略改了,需要声明一个新的队列
即便是上述设置,也不能完全保证不会丢失数据,因为可能rabbitMQ还没将数据存到磁盘里。如果想要一个鲁棒性强的,见这里。
通过设置consumer,可以实现每个consumer至多消费一个message,也就是说如果当前consumer处于busy,则它不会再接收message,message会由server分发给not busy的consumer:
channel.basicQos(1);
第三步
Exchange,位于producer和queue之间,在rabbitMQ中,并不是producer直接发送message到queue中,而是将message发送到exchange里,再由对应type的exchange去push到queue中;
如果使用不带参数的方法queueDeclare()
我们可以新建一个非持久化,独有的,可自动删除并带有随机名字的队列;
当我们创建fanout的exchange和一个queue时,现在我们需要告诉exchange发送message到我们的queue中。这个在exchange和queue之间的关系叫做binding。
总的来说,设置发布/订阅模式,通过server设置exchange
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
并且在注册的时候注意
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
而consumer则通过binding exchange和queue,来实现扇出
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, EXCHANGE_NAME, "");
第四步
通过设置exchange的type为direct,可以实现对应的路由算法——message会送到binding key完全和它的routing key匹配的queues中。message的routing key和queue的binding key互相匹配。
首先我们需要在producer中设置exchange的type
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
然后发送准备发送消息
channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes()); // severity就是routing key
接着需要在consumer中设置binding
for (String severity : args){
channel.queueBind(queueName, EXCHANGE_NAME, severity);
}
第五步
通过设置exchange的type为topic,可以实现得到固定源的对应routing key 的message
相当于在direct上更进一步,可获取相应源的message了。
配置上和direct基本相同,可以认为direct就是特殊的topic
第六步
和RPC相关,有点复杂,这里不过多阐述了。
另外我简单的翻译了两篇官方的文档,能够更好的帮助理解rabbitmq,分别是对于确认和队列阐述。
Spring boot RabbitMQ
当我们对于rabbitmq有一定了解之后,就需要实际使用它了,而spring boot刚好有关于rabbitmq的整合,那就是spring-boot-amqp包。
这里学习,我分别参考了如下几篇文章:
https://www.cnblogs.com/skychenjiajun/p/9037324.html
https://www.cnblogs.com/boshen-hzb/p/6841982.html
https://blog.csdn.net/qq_38455201/article/details/80308771
由于相关文章的内容重复性比较高,上述几篇也基本上绝大部分内容都是重叠的。
以及spring官方给的关于amqp这个包的示例程序:
https://github.com/spring-projects/spring-amqp-samples
基本上有了这些示例,大概就能对于rabbitmq的使用有一个比较初级的认识了,可以尝试写一些测试用例跑跑了。之后再根据具体的业务需求不断地学习和精进。
下面简单地写写相关内容,不写地太详细进行重复劳动了,具体可参见上面贴出来的几篇文章。
首先我们根据之前对于rabbitmq地学习,大概知道了queue、routingkey、exchange这样几个概念,所以首先我们就需要在配置中配置;当然,最必不可少地配置是如下两个配置:
@Bean
public ConnectionFactory connectionFactory(){
CachingConnectionFactory connectionFactory = new CachingConnectionFactory(host, port);
connectionFactory.setUsername(username);
connectionFactory.setPassword(password);
connectionFactory.setVirtualHost("/");
connectionFactory.setPublisherConfirms(true);
return connectionFactory;
}
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public RabbitTemplate rabbitTemplate(){
RabbitTemplate template = new RabbitTemplate(connectionFactory());
return template;
}
接下来才是对于queue、binding、exchange地配置
/**
* 针对消费者配置
* 1. 设置交换机类型
* 2. 将队列绑定到交换机
FanoutExchange: 将消息分发到所有的绑定队列,无routingkey的概念
HeadersExchange :通过添加属性key-value匹配
DirectExchange:按照routingkey分发到指定队列
TopicExchange:多关键字匹配
*/
@Bean
public DirectExchange defaultExchange(){
return new DirectExchange(EXCHANGE_A);
}
/**
* 获取队列A
* @return
*/
@Bean
public Queue queueA(){
return new Queue(QUEUE_A, true); // 队列持久
}
@Bean
public Binding binding(){
return BindingBuilder.bind(queueA()).to(defaultExchange()).with(RabbitConfig.ROUTINGKEY_A);
}
对于fanout
和topic
的exchange配置略有不同:
/**
* 下面是关于fanout的配置
*/
@Bean
FanoutExchange fanoutExchange(){
return new FanoutExchange(RabbitConfig.FANOUT_EXCHANGE); // RabbitConfig.FANOUT_EXCHANGE="my-mq-fanout-exchange"
}
@Bean
TopicExchange exchange(){
return new TopicExchange("exchange");
}
当然我们也可以在这个配置类中直接配置一个相应地消费信息:
// 就是一个消费者,也接收队列中的消息
@Bean
public SimpleMessageListenerContainer messageContainer(){
//加载处理消息A的队列
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory());
//设置接收多个队列里面的消息,这里设置接收队列A
//假如想一个消费者处理多个队列里面的信息可以如下设置:
//container.setQueues(queueA(),queueB(),queueC());
container.setQueues(queueA());
container.setExposeListenerChannel(true);
//设置最大的并发的消费者数量
container.setMaxConcurrentConsumers(10);
//最小的并发消费者的数量
container.setConcurrentConsumers(1);
//设置确认模式手工确认
container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
container.setMessageListener(new ChannelAwareMessageListener() {
@Override
public void onMessage(Message message, Channel channel) throws Exception {
/**通过basic.qos方法设置prefetch_count=1,这样RabbitMQ就会使得每个Consumer在同一个时间点最多处理一个Message,
换句话说,在接收到该Consumer的ack前,它不会将新的Message分发给它 */
channel.basicQos(1);
byte[] body = message.getBody();
logger.info("新的方式接收处理队列A当中的消息:" + new String(body));
/**为了保证永远不会丢失消息,RabbitMQ支持消息应答机制。
当消费者接收到消息并完成任务后会往RabbitMQ服务器发送一条确认的命令,然后RabbitMQ才会将消息删除。*/
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
});
return container;
}
接下来我们简单配置一个producer:
package com.yubotao.rabbitmq_with_sb.producer;
import com.yubotao.rabbitmq_with_sb.config.RabbitConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.UUID;
/**
* @Auther: yubt
* @Description:
* @Date: Created in 10:11 2018/11/22
* @Modified By:
*/
@Component
public class MsgProducer implements RabbitTemplate.ConfirmCallback {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
// 由于rabbitTemplate的scope属性设置为ConfigurableBeanFactory.SCOPE_PROTOTYPE,所以不能自动注入
private RabbitTemplate rabbitTemplate;
@Autowired
public MsgProducer(RabbitTemplate rabbitTemplate){
this.rabbitTemplate = rabbitTemplate;
// rabbitTemplate如果为单例的话,那回调就是最后设置的内容
rabbitTemplate.setConfirmCallback(this);
}
public void sendMsg(String content){
CorrelationData correlationId = new CorrelationData(UUID.randomUUID().toString());
//把消息放入ROUTINGKEY_A对应的队列当中去,对应的是队列A
rabbitTemplate.convertAndSend(RabbitConfig.EXCHANGE_A, RabbitConfig.ROUTINGKEY_A, content, correlationId);
}
public void sendAll(String content){
rabbitTemplate.convertAndSend(RabbitConfig.FANOUT_EXCHANGE, "", content);
}
// 回调
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause){
logger.info(" 回调id:" + correlationData);
if (ack){
logger.info("消息成功消费");
}else {
logger.info("消息消费失败:" + cause);
}
}
}
以及一个简单的consumer:
package com.yubotao.rabbitmq_with_sb.receiver;
import com.yubotao.rabbitmq_with_sb.config.RabbitConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @Auther: yubt
* @Description:
* @Date: Created in 10:26 2018/11/22
* @Modified By:
*/
@Component
@RabbitListener(queues = {RabbitConfig.QUEUE_A, RabbitConfig.QUEUE_B, RabbitConfig.QUEUE_C})
public class MsgReceiver {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@RabbitHandler
public void process(String content){
logger.info("接收处理队列A或B或C当中的消息: " + content);
}
}
然后写两个测试:
package com.yubotao.rabbitmq_with_sb;
import com.yubotao.rabbitmq_with_sb.producer.MsgProducer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
public class RabbitmqWithSbApplication {
@Autowired
MsgProducer msgProducer;
public static void main(String[] args) {
SpringApplication.run(RabbitmqWithSbApplication.class, args);
}
@RequestMapping(value = "/testMQ", method = RequestMethod.POST)
public void test(@RequestParam("msg") String msg){
msgProducer.sendMsg(msg);
}
@RequestMapping(value = "/testMQ/sendAll", method = RequestMethod.POST)
public void sendAll(@RequestParam("msg") String msg){
msgProducer.sendAll(msg);
}
}
在具体一些的内容就可以看GitHub中的代码了。不过其实并没有太多的多余信息了。
学习使用中遇到的问题
首先是对于消息队列使用的思考。
我们在使用消息队列的时候,我们需要为producer提供一个接口,然后将相关的message放入队列中。即Controller中提供相关接口去调用我们写的producer类中的相关方法。
而consumer类中,主要需要配置对应的几个注解——@Component
、@RabbitListener
、@RabbitHandler
在对应方法上有相关的@RabbitListener
、@RabbitHandler
,就会直接消费掉对应队列中存在的message。
接着是和上面贴的示例代码不同的是,当我们对于producer的方法入参封装成message时,@RabbitListener要变成方法级的,否则会报错:
Caused by: org.springframework.amqp.AmqpException: No method found for class String
对应的producer中的方法:
public void sendMsg(String content){
CorrelationData correlationId = new CorrelationData(UUID.randomUUID().toString());
Message message = MessageBuilder.withBody(content.getBytes())
.andProperties(MessagePropertiesBuilder.newInstance().setContentType("application/json").build())
.build();
//把消息放入ROUTINGKEY对应的队列当中去,对应的是binding的相关队列
rabbitTemplate.send(RabbitConfig.EXCHANGE, RabbitConfig.ROUTINGKEY, message, correlationId);
}
而我们在将消息放到队列中,需要考虑很多相关问题:
我们如何保证message被消费成功?(ack)
如果ack失败,message回滚队列且无限循环这个过程怎么办?(需要设置相应的机制在适当的时候抛弃这条message)
消息队列如果挂掉怎么办?(涉及运维方面的问题,要考虑动态扩容,channel切换,选举等等一系列可能涉及的问题)
所以我们在项目中加入消息队列时,就需要考虑到消息队列可能带来的负面影响;我们需要在producer中保证message入队前的正确性,以防影响consumer端消费的业务逻辑报错对于队列的相关影响;同时我们需要在consumer端去处理message递送交付的相关问题,以及在producer中无法提前处理的相关问题,比如设置ack,在多次重试后抛弃相应的message等等。
如下是在consumer端设置ack:
// @RabbitListener放到类上会报错,但是放到方法级是没问题的。之前直接处理String的时候,放到类级上可行。
@RabbitListener(queues = RabbitConfig.QUEUE)
@RabbitHandler
public void process(Message message, Channel channel) throws Exception {
boolean flag = false;
logger.info("接收处理队列当中的消息: " + new String(message.getBody()));
try {
JSONObject combinedMap = (JSONObject) JSONObject.parse(message.getBody());
String designId = (String) combinedMap.get("xxId");
String jsonData = (String) combinedMap.get("jsonData");
CommonUtils.checkParamsIsEmpty(designId, jsonData);
// 相应的业务逻辑方法
objectService.sendObjectData(designId, jsonData);
flag = true;
}catch (Exception e){
e.printStackTrace();
}
// 判断业务逻辑是否成功,message是否持久化, 否则回滚队列
if (flag) {
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}else {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
关于Spring boot RabbitMQ的补充知识
内容来自这里。
当Rabbit基础设施存在时,任何被声明为@RabbitListener
的bean都会创建一个监听者端点。如果未定义RabbitListenerContainerFactory
,通常自动配置默认的SimpleRabbitListenerContainerFactory
,你可以通过使用spring.rabbitmq.listener.type
属性选择相应的容器。如果定义了MessageConverter
或MessageRecoverer
的bean,它通常自动和默认工厂关联。
如果你需要更多的RabbitListenerContainerFactory
实例或者你想重载默认的,spring boot提供了SimpleRabbitListenerContainerFactoryConfigurer
和DirectRabbitListenerContainerFactoryConfigurer
,让你通过自动配置工厂使用的相同设置来初始化SimpleRabbitListenerContainerFactory
和DirectRabbitListenerContainerFactory
。
例如,下面的配置类就使用一个特定的MessageConverter
来暴露另一个工厂:
@Configuration
static class RabbitConfiguration {
@Bean
public SimpleRabbitListenerContainerFactory myFactory(
SimpleRabbitListenerContainerFactoryConfigurer configurer) {
SimpleRabbitListenerContainerFactory factory =
new SimpleRabbitListenerContainerFactory();
configurer.configure(factory, connectionFactory);
factory.setMessageConverter(myMessageConverter());
return factory;
}
}
我们可以在任何@RabbitListener
处使用这个工厂:
@Component
public class MyBean {
@RabbitListener(queues = "someQueue", containerFactory="myFactory")
public void processMessage(String content) {
// ...
}
}
你可以使用重试来处理你的监听者抛出异常这种情况。默认使用RejectAndDontRequeueRecoverer
,但你可以定义你自己的MessageRecoverer
。当重试次数耗尽,message被拒绝,根据broker的配置或者断开连接,或者路由到一个死信exchange。默认下不允许重试,你可以通过声明RabbitRetryTemplateCustomizer
的bean以编程的方式自定义RetryTemplate
。
重要提示
默认情况下,如果重试被禁用,监听者抛出异常,delivery无限重复。你可以通过两种方式改变这一行为:设置defaultRequeueRejected
为false,这样就不会再次delivery,或者抛出一个AmqpRejectAndDontRequeueException
异常来表示message应该被拒绝。另一个机制是开启重试,并设置delivery尝试送达的最大值。
关于spring boot properties中属性配置
rabbitmq的配置是通过这个类来实现的。
以及一个简洁的中文翻译的参考版。
最后,spring-amqp的官方文档,内容较多,暂时没有翻看,后续深入学习的时候就是主要参考文档了。
2019.6.6 新增动态管理队列
根据业务需求,有时候可能需要我们动态的对队列进行新增或删除,而不是项目一开始就指定队列名那种做法。当需要对队列进行管理的时候,就需要使用rabbitAdmin
这个类。
同时配合上相应的方法
public String addQueue(Queue queue){
return rabbitAdmin.declareQueue(queue);
}
public boolean deleteQueue(String queueName){
return rabbitAdmin.deleteQueue(queueName);
}
以下代码内容主要参考如下两篇文章:
即时使用工具类
略带瑕疵的配置
配置类RabbitConnectionConfig.java
package com.yubotao.dynamicQueue.config;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Auther: yubotao
* @Description:
* @Date: Created in 13:46 2019/5/31
* @Modified By:
*/
@Configuration
public class RabbitConnectionConfig {
@Value("${spring.rabbitmq.host}")
private String host;
@Value("${spring.rabbitmq.port}")
private int port;
@Value("${spring.rabbitmq.username}")
private String username;
@Value("${spring.rabbitmq.password}")
private String password;
@Bean
public ConnectionFactory mqConnectionFactory(){
CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
connectionFactory.setUsername(username);
connectionFactory.setPassword(password);
connectionFactory.setVirtualHost("/");
connectionFactory.setPublisherConfirms(true);
// 设置通道数量
connectionFactory.setChannelCacheSize(40);
// 该方法配置多个host,在当前连接host down掉的时候会自动去重连后面的host
connectionFactory.setAddresses(host);
return connectionFactory;
}
@Bean
public RabbitAdmin rabbitAdmin(){
return new RabbitAdmin(mqConnectionFactory());
}
}
工具类RabbitUtil.java
package com.yubotao.dynamicQueue.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.*;
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.stereotype.Component;
import javax.annotation.Resource;
/**
* @Auther: yubotao
* @Description:
* @Date: Created in 13:54 2019/5/31
* @Modified By:
*/
@Component
public class RabbitUtil {
private Logger logger = LoggerFactory.getLogger(this.getClass());
private static final String DIRECT_EXCHANGE_NAME = "test-dynamic";
@Resource
private RabbitAdmin rabbitAdmin;
@Resource
private RabbitTemplate rabbitTemplate;
// 配置发送格式
@Bean
public AmqpTemplate amqpTemplate(){
// 使用jackson 消息转换器
rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
rabbitTemplate.setEncoding("UTF-8");
// 开启return callback
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
String correlationId = message.getMessageProperties().getCorrelationId();
logger.info("消息:{} 发送失败, 应答码:{} 原因:{} 交换机: {} 路由键: {}", correlationId,replyCode,replyText,exchange,routingKey);
});
// 消息确认
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (ack){
logger.info("消息发送到exchange成功");
}else {
logger.info("消息发送到exchange失败,原因: {}", cause);
}
});
return rabbitTemplate;
}
public Message getMessage(String messageType, Object msg){
MessageProperties messageProperties = new MessageProperties();
messageProperties.setContentType(messageType);
Message message = new Message(msg.toString().getBytes(), messageProperties);
return message;
}
public void sendToQueue(String queueName, String message){
DirectExchange exchange = createExchange(DIRECT_EXCHANGE_NAME);
addExchange(exchange);
Queue queue = createQueue(queueName);
addQueue(queue);
addBinding(queue, exchange, queueName);
rabbitTemplate.convertAndSend(exchange.getName(), queueName, message);
}
public String receiveFromQueue(String queueName){
String message = (String)rabbitTemplate.receiveAndConvert(queueName);
System.out.println("Receive: " + message);
return message;
}
public DirectExchange createExchange(String exchangeName){
return new DirectExchange(exchangeName, true, false);
}
public Queue createQueue(String queueName){
return QueueBuilder.durable(queueName).build();
}
/**
* 创建时限队列,可自动删除
* @param queueName
* @return
*/
public Queue createExpiresQueue(String queueName){
return QueueBuilder.durable(queueName)
// .withArgument("x-message-ttl", delayMillis) // 死信时间
.withArgument("x-expires", 3000) // 设置队列自动删除时间
// .withArgument("x-dead-letter-exchange", rabbitConfig.getExchange()) // 死信重新投递的交换机
// .withArgument("x-dead-letter-routing-key", rabbitConfig.getQueue()) // 路由到队列的routingKey
.build();
}
/**
* 使用一个routingKey绑定一个队列到一个匹配型交换器
* @param queue
* @param exchange
* @param routingKey
*/
public void addBinding(Queue queue, DirectExchange exchange, String routingKey){
Binding binding = BindingBuilder.bind(queue).to(exchange).with(routingKey);
rabbitAdmin.declareBinding(binding);
}
/**
* 创建一个指定的Queue
* @param queue
* @return queueName
*/
public String addQueue(Queue queue){
return rabbitAdmin.declareQueue(queue);
}
public boolean deleteQueue(String queueName){
return rabbitAdmin.deleteQueue(queueName);
}
/**
* 创建Exchange
* @param exchange
*/
public void addExchange(AbstractExchange exchange){
rabbitAdmin.declareExchange(exchange);
}
public boolean deleteExchange(String exchangeName){
return rabbitAdmin.deleteExchange(exchangeName);
}
}