Docker 安装 RabbitMQ
docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 \
-p 25672:25672 -p 15671:15671 -p 15672:15672 \
rabbitmq:management
4369、25672(Erlang发现&集群端口)
5672、5671(AMQP端口)
15672(web管理后台端口)
61613、61614(STOMP协议端口)
1883、8883(MQTT协议端口)
RabbitMQ的使用
RabbitMQ运行机制
AMQP 中消息的路由过程:生产者把消息发布到 Exchange上,消息最终到达队列并被消费者接收,而 Binding 决定了 Exchange 的消息应该发送到哪个队列。
Exchange 类型
分发消息时,Exchange 类型不同,分发策略会有所区别,目前共有四种类型:direct、fanout、topic、headers。
-
headers:匹配 AMQP 消息的 header 而不是路由键,headers 交换机和 direct 交换机完全一致,但是性能差很大,目前几乎不用。
-
direct exchange:消息中的路由键(routing key)如果和 Binding中的 binding key一致,交换机 Exchange 就将消息发送到对应的队列中。路由键与队列名完全匹配,如果一个队列绑定到交换机要求路由键为 cat,则只转发 routing key 标记为 cat 的消息,不会转发 cat.a,也不会转发 cat.b 等等消息。它是完全匹配,单播的模式。
-
Fanout Exchange:每个发送到 fanout 类型的交换机的消息都会分到所有绑定的队列上去。fanout 交换机不处理路由键,只是简单的将队列绑定到交换器上,每个发送的交换器的消息都会被转发到与该交换机绑定的所有队列上。很像子网广播。
-
Topic Exchange:topic 交换机通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配。此时队列需要绑定到一个模式上。它将路由键和绑定的字符串切分成单词。这些单词之间用点隔开。同样也会识别通配符:符号“#”和符号“*”。“#”匹配0个或多个单词,“*” 匹配一个单词
创建 Direct 类型的交换机
创建队列
在 Exchange 中创建到 Queue的绑定 Binding
创建Queue 和 Exchange
SpringBoot 整合 RabbitMQ
-
引入 RabbitMQ 场景启动器
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
引入场景启动器后,RabbitAutoConfiguration 配置类就会自动生效。查看配置类:
@Configuration(proxyBeanMethods = false) @ConditionalOnClass({ RabbitTemplate.class, Channel.class }) @EnableConfigurationProperties(RabbitProperties.class) @Import(RabbitAnnotationDrivenConfiguration.class) public class RabbitAutoConfiguration { @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(ConnectionFactory.class) protected static class RabbitConnectionFactoryCreator { @Bean public CachingConnectionFactory rabbitConnectionFactory(RabbitProperties properties, ResourceLoader resourceLoader, ObjectProvider<CredentialsProvider> credentialsProvider, ObjectProvider<CredentialsRefreshService> credentialsRefreshService, ObjectProvider<ConnectionNameStrategy> connectionNameStrategy, ObjectProvider<ConnectionFactoryCustomizer> connectionFactoryCustomizers) throws Exception { ... return factory; } } @Configuration(proxyBeanMethods = false) @Import(RabbitConnectionFactoryCreator.class) protected static class RabbitTemplateConfiguration { ... @Bean @ConditionalOnSingleCandidate(ConnectionFactory.class) @ConditionalOnMissingBean(RabbitOperations.class) public RabbitTemplate rabbitTemplate(RabbitTemplateConfigurer configurer, ConnectionFactory connectionFactory) { RabbitTemplate template = new RabbitTemplate(); configurer.configure(template, connectionFactory); return template; } @Bean @ConditionalOnSingleCandidate(ConnectionFactory.class) @ConditionalOnProperty(prefix = "spring.rabbitmq", name = "dynamic", matchIfMissing = true) @ConditionalOnMissingBean public AmqpAdmin amqpAdmin(ConnectionFactory connectionFactory) { return new RabbitAdmin(connectionFactory); } } @Configuration(proxyBeanMethods = false) @ConditionalOnClass(RabbitMessagingTemplate.class) @ConditionalOnMissingBean(RabbitMessagingTemplate.class) @Import(RabbitTemplateConfiguration.class) protected static class MessagingTemplateConfiguration { @Bean @ConditionalOnSingleCandidate(RabbitTemplate.class) public RabbitMessagingTemplate rabbitMessagingTemplate(RabbitTemplate rabbitTemplate) { return new RabbitMessagingTemplate(rabbitTemplate); } } }
引入amqp场景启动后,容器中放入了
rabbitConnectionFactory
、rabbitMessagingTemplate
、amqpAdmin
、rabbitTemplate
这四个组件。 -
在 application.yml 中配置 RabbitMQ 的连接,需要配置的属性,可以通过查看 RabbitProperties 类获取。
spring: rabbitmq: host: 192.168.94.137 port: 5672 virtual-host: /
-
在启动类上标注 @EnableRabbit 注解,开启对 RabbitMq 的支持 。
-
单元测试:
-
测试使用 amqpAdmin 创建Exchang、Queue、Binding
@Autowired AmqpAdmin amqpAdmin; /** * 创建 Exchange */ @Test void createExchange() { Exchange exchange = new DirectExchange("java-direct-exchange", true, false); amqpAdmin.declareExchange(exchange); } /** * 创建 Queue */ @Test void createQueue() { // exclusive true表明该队列只能被声明的连接使用 Queue queue = new Queue("java-queue", true, false, false); amqpAdmin.declareQueue(queue); } /** * 创建 Binding */ @Test void createBinding() { Map<String, Object> map = new HashMap<>(); Binding binding = new Binding("java-queue", Binding.DestinationType.QUEUE, "java-direct-exchange", "java-direct-exchange", map); amqpAdmin.declareBinding(binding); }
-
测试使用 RabbitTemplate 发送消息
@Autowired RabbitTemplate rabbitTemplate; @Test void sendMessage() { User user = new User(); user.setName("陈李张"); user.setSex("男"); rabbitTemplate.convertAndSend("java-direct-exchange", "java-direct-exchange", user); }
在 RabbitMQ 的web页面中,我们可以看到消息的内容,该内容是被 java 序列化后内容:
看一下 RabbitAutoConfiguration类中 创建 RabbitTemplate 源码:@Configuration(proxyBeanMethods = false) @Import(RabbitConnectionFactoryCreator.class) protected static class RabbitTemplateConfiguration { @Bean @ConditionalOnMissingBean public RabbitTemplateConfigurer rabbitTemplateConfigurer(RabbitProperties properties, ObjectProvider<MessageConverter> messageConverter, ObjectProvider<RabbitRetryTemplateCustomizer> retryTemplateCustomizers) { RabbitTemplateConfigurer configurer = new RabbitTemplateConfigurer(); configurer.setMessageConverter(messageConverter.getIfUnique()); configurer .setRetryTemplateCustomizers(retryTemplateCustomizers.orderedStream().collect(Collectors.toList())); configurer.setRabbitProperties(properties); return configurer; } @Bean @ConditionalOnSingleCandidate(ConnectionFactory.class) @ConditionalOnMissingBean(RabbitOperations.class) public RabbitTemplate rabbitTemplate(RabbitTemplateConfigurer configurer, ConnectionFactory connectionFactory) { RabbitTemplate template = new RabbitTemplate(); configurer.configure(template, connectionFactory); return template; } ... }
我们发现容器中放了 RabbitTemplateConfigurer 组件,而该组件构造时候用到了参数
ObjectProvider<MessageConverter> messageConverter
,而Provider 类型的参数表明:如果 Spring 的容器中有该MessageConverter类型的组件,就会使用容器中的,如果容器中没有,就会使用默认的。
RabbitTemplate 类的源码,发现如下一行代码:private MessageConverter messageConverter = new SimpleMessageConverter();
因此,我们知道,容器中如果没有自定义的MessageConverter的情况下,会使用默认的SimpleMessageConverter();
SimpleMessageConverter源码:public class SimpleMessageConverter extends AllowedListDeserializingMessageConverter implements BeanClassLoaderAware { ... /** * Converts from a AMQP Message to an Object. */ @Override public Object fromMessage(Message message) throws MessageConversionException { Object content = null; MessageProperties properties = message.getMessageProperties(); if (properties != null) { String contentType = properties.getContentType(); if (contentType != null && contentType.startsWith("text")) { String encoding = properties.getContentEncoding(); if (encoding == null) { encoding = this.defaultCharset; } try { content = new String(message.getBody(), encoding); } catch (UnsupportedEncodingException e) { throw new MessageConversionException( "failed to convert text-based Message content", e); } } else if (contentType != null && contentType.equals(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT)) { try { content = SerializationUtils.deserialize( createObjectInputStream(new ByteArrayInputStream(message.getBody()), this.codebaseUrl)); } catch (IOException | IllegalArgumentException | IllegalStateException e) { throw new MessageConversionException( "failed to convert serialized Message content", e); } } } if (content == null) { content = message.getBody(); } return content; } /** * Creates an AMQP Message from the provided Object. */ @Override protected Message createMessage(Object object, MessageProperties messageProperties) throws MessageConversionException { byte[] bytes = null; if (object instanceof byte[]) { bytes = (byte[]) object; messageProperties.setContentType(MessageProperties.CONTENT_TYPE_BYTES); } else if (object instanceof String) { try { bytes = ((String) object).getBytes(this.defaultCharset); } catch (UnsupportedEncodingException e) { throw new MessageConversionException( "failed to convert to Message content", e); } messageProperties.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN); messageProperties.setContentEncoding(this.defaultCharset); } else if (object instanceof Serializable) { try { bytes = SerializationUtils.serialize(object); } catch (IllegalArgumentException e) { throw new MessageConversionException( "failed to convert to serialized Message content", e); } messageProperties.setContentType(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT); } if (bytes != null) { messageProperties.setContentLength(bytes.length); return new Message(bytes, messageProperties); } throw new IllegalArgumentException(getClass().getSimpleName() + " only supports String, byte[] and Serializable payloads, received: " + object.getClass().getName()); } ... }
看 createMessage 方法后发现,如果消息对应的类型是String类型,那么直接将对象强转为String类型;如果对应的消息类型实现了序列化接口,那么会使用序列化工具,将对象转换为Byte数组。
如果要想变换消息转换策略,我们就要看一下有哪些MessageConverter
看到这里,我们可以向容器中放一个 Jackson2JsonMessageConverter 组件,那么消息转换时,对象就能转换成JSON。import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MyRabbitConfig { @Bean public MessageConverter messageConverter() { return new Jackson2JsonMessageConverter(); } }
再次查看 web页面中的消息,已经是json格式的消息了,如下:
-
-
方法上标注 @RabbitListener 监听消息(也可以标注在类上)
@RabbitListener(queues = {"java-queue"}) public void receiveMessage(Object message) { System.out.println("receive message : " + message); }
receive message : (Body:’[B@624d7aa9(byte[102])’ MessageProperties [headers={}, contentType=application/x-java-serialized-object, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=true, receivedExchange=java-direct-exchange, receivedRoutingKey=java-direct-exchange, deliveryTag=1, consumerTag=amq.ctag-Jm-YZOeWgvMtTa8Q_VWXrQ, consumerQueue=java-queue])
receive message : (Body:’{“name”:“陈李张”,“sex”:“男”}’ MessageProperties [headers={TypeId=com.feng.mall.order.User}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=true, receivedExchange=java-direct-exchange, receivedRoutingKey=java-direct-exchange, deliveryTag=2, consumerTag=amq.ctag-Jm-YZOeWgvMtTa8Q_VWXrQ, consumerQueue=java-queue]) -
方法上标注 @RabbitHandler 注解,以接收不同类型的消息
@RabbitListener(queues = {"java-queue"}) @Service("orderItemService") public class OrderItemServiceImpl extends ServiceImpl<OrderItemDao, OrderItemEntity> implements OrderItemService { @RabbitHandler public void receiveMessage(Message message, Order order, Channel channel) { System.out.println("receive message : " + message); } @RabbitHandler public void receiveMessage(Message message, User user, Channel channel) { } }
如果要接收同一个队列中两种不同类型的消息,或者不同队列中,不同类型的消息。可以使用 @RabbitListener 监听队列,而用@RabbitHandler 重载方法,以处理不同消息类型。
RabbitMQ消息确认机制-可靠抵达
参考文档
发送端 Publisher 有两次回调:
confirmCallback,消息发送者将消息发送到 Broker 后回调
returnCallback,Exchange 未将消息发送到 Queue 之后的回调
接收端 Consumer 有一次回调:
ack,在 Consumer 收到消息之后回调
- 可靠抵达 - ConfirmCallback
消息只要被 Broker 接收到就会执行 confirmCallback。如果是集群模式,需要所有的 Broker 都接收到才会调用 confirmCallback。spring: rabbitmq: publisher-confirms: true
被 Broker 接收到只表示 message 已经到达了服务器,并不能保证消息一定会被投递到目标 Queue 里。因此需要 returnCallback。 - 可靠抵达- ReturnCallback
confirm 模式里只能保证消息到达 Broker,不能保证消息准确投递到目标 Queue 里。在有些业务场景下,我们需要保证消息一定要投递到目标 Queue 里,此时就需要用到 return 退回模式。spring: rabbitmq: publisher-returns: true template: mandatory: true
消息未能投递到目标 Queue 里,将调用 returnCallback,可以记录下详细的投递数据,以备定期巡检或者自动纠错。 - 定制 RabbitTemplate,监听消息投递过程中的两次回调
@Configuration public class MyRabbitConfig { @Autowired RabbitTemplate rabbitTemplate; @Bean public MessageConverter messageConverter() { return new Jackson2JsonMessageConverter(); } @PostConstruct public void initRabbitTemplate() { rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() { /** * 该方法会在消息到达 Broker 之后回调 * @param correlationData 当前消息唯一关联数据(这个是消息的唯一 id) * @param ack 消息是否成功收到 * @param cause 失败的原因 */ @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { } }); rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() { /** * ReturnedMessage 类的成员变量 * Message message; 投递失败的消息的详细信息 * int replyCode; 回复的状态码 * String replyText; 回复的文本内容 * String exchange; 当时接收这个消息的交换机 * String routingKey;当时发送这个消息使用的路由键 * * @param returned */ @Override public void returnedMessage(ReturnedMessage returned) { } }); } }
- 可靠抵达 - Ack消息确认机制
默认情况下,消息是自动确认的。消息被确认收到后,会从队列中删除。但是这种自动确认模式有一个弊端:假如队列中有10条消息,但是我们在确认第2条消息的时候,程序运行出现的异常。再次查看队列,队列中已经没有消息了。也就是自动确认模式,在有异常出现的情况下会丢失消息。
手动确认配置:
手动模式下,只要我们没有明确告诉MQ,消息被ACK,消息就会一直在队列中。即使 Consumer 宕机,消息也不会丢失。spring: rabbitmq: listener: simple: acknowledge-mode: manual
消息处理成功,就需要ACK,调用 basicAck 方法:
basicNack方法,一般在业务失败时候调用@RabbitHandler public void receiveMessage(Message message, Channel channel) { long deliveryTag = message.getMessageProperties().getDeliveryTag(); try { channel.basicAck(deliveryTag, false); } catch (IOException e) { } }
// deliveryTag 消息标识, multiple true 批量退回No ACK,requeue true 重写入队,false 丢弃 void basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException;