消息的可靠性
消息确认机制
确认并且保证消息被送达,提供了两种方式:发布确认和事务。(两者不可同时使用)在channel为事务时,不可引入确认模式;同样channel为确认模式下,不可使用事务。
发布确认
有两种方式:消息发送成功确认和消息发送失败回调。
- 消息发送成功确认
在spring-rabbitmq-producer\src\main\resources\spring\spring-rabbitmq.xml
connectionFactory 中启用消息确认:
<!-- publisher-confirms="true" 表示:启用了消息确认 --> <rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}" port="${rabbitmq.port}" username="${rabbitmq.username}" password="${rabbitmq.password}" virtual-host="${rabbitmq.virtual-host}" publisher-confirms="true" />
配置消息确认回调方法如下:
<!-- 消息回调处理类 --> <bean id="confirmCallback" class="com.itheima.rabbitmq.MsgSendConfirmCallBack"/> <!--定义rabbitTemplate对象操作可以在代码中方便发送消息--> <!-- confirm-callback="confirmCallback" 表示:消息失败回调 --> <rabbit:template id="rabbitTemplate" connection-factory="connectionFactory" confirm-callback="confirmCallback"/>
消息确认回调方法com.itheima.rabbitmq.MsgSendConfirmCallBack如下:
public class MsgSendConfirmCallBack implements RabbitTemplate.ConfirmCallback { public void confirm(CorrelationData correlationData, boolean ack, String cause) { if (ack) { System.out.println("消息确认成功...."); } else { //处理丢失的消息 System.out.println("消息确认失败," + cause); } } }
功能测试如下:
发送消息
com.itheima.rabbitmq.ProducerTest#queueTest
@Test public void queueTest(){ //路由键与队列同名 rabbitTemplate.convertAndSend("spring_queue", "只发队列spring_queue的消息。"); }
管理界面确认消息发送成功
消息确认回调
- 消息发送失败回调
在spring-rabbitmq-producer\src\main\resources\spring\spring-rabbitmq.xml
connectionFactory 中启用回调:
<!-- publisher-returns="true" 表示:启用了失败回调 --> <rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}" port="${rabbitmq.port}" username="${rabbitmq.username}" password="${rabbitmq.password}" virtual-host="${rabbitmq.virtual-host}" publisher-returns="true" />
配置消息失败回调方法如下:
注意:同时需配置mandatory="true",否则消息则丢失
<!-- 消息失败回调类 --> <bean id="sendReturnCallback" class="com.itheima.rabbitmq.MsgSendReturnCallback"/> <!-- return-callback="sendReturnCallback" 表示:消息失败回调 ,同时需配置mandatory="true",否则消息则丢失--> <rabbit:template id="rabbitTemplate" connection-factory="connectionFactory" confirm-callback="confirmCallback" return-callback="sendReturnCallback" mandatory="true"/>
消息失败回调方法com.itheima.rabbitmq.MsgSendReturnCallback如下:
public class MsgSendReturnCallback implements RabbitTemplate.ReturnCallback { public void returnedMessage(Message message, int i, String s, String s1, String s2) { String msgJson = new String(message.getBody()); System.out.println("Returned Message:"+msgJson); } }
功能测试如下:
模拟消息发送失败
com.itheima.rabbitmq.ProducerTest#testFailQueueTest
@Test public void testFailQueueTest() throws InterruptedException { //exchange 正确,queue 错误 ,confirm被回调, ack=true; return被回调 replyText:NO_ROUTE amqpTemplate.convertAndSend("test_fail_exchange", "", "测试消息发送失败进行确认应答。"); }
失败回调结果如下:
事务支持
场景:业务处理伴随消息的发送,业务处理失败(事务回滚)后要求消息不发送。rabbitmq 使用调用者的外部事务,通常是首选,因为它是非侵入性的(低耦合)。
外部事务的配置:spring-rabbitmq-producer\src\main\resources\spring\spring-rabbitmq.xml
<!-- channel-transacted="true" 表示:支持事务操作 --> <rabbit:template id="rabbitTemplate" connection-factory="connectionFactory" confirm-callback="confirmCallback" return-callback="sendReturnCallback" channel-transacted="true" /> <!--平台事务管理器--> <bean id="transactionManager" class="org.springframework.amqp.rabbit.transaction.RabbitTransactionManager"> <property name="connectionFactory" ref="connectionFactory"/> </bean>
- 模拟业务处理失败的场景:
测试类或者测试方法上加入@Transactional注解
@Transactional public class ProducerTest
@Test public void queueTest2(){ //路由键与队列同名 rabbitTemplate.convertAndSend("spring_queue", "只发队列spring_queue的消息--01。"); System.out.println("----------------dosoming:可以是数据库的操作,也可以是其他业务类型的操作---------------"); //模拟业务处理失败 System.out.println(1/0); rabbitTemplate.convertAndSend("spring_queue", "只发队列spring_queue的消息--02。"); }
测试结果:
Consumer Ack
ack——acknowledge(vt. 承认;答谢;报偿;告知已收到),在RabbitMQ中指代的是消费者收到消息后确认的一种行为,关注点在于消费者能否实际接收到MQ发送的消息。
其提供了三种确认方式:
自动确认acknowledge="none":当消费者接收到消息的时候,就会自动给到RabbitMQ一个回执,告诉MQ我已经收到消息了,不在乎消费者接收到消息之后业务处理的成功与否。
手动确认acknowledge="manual":当消费者收到消息后,不会立刻告诉RabbitMQ已经收到消息了,而是等待业务处理成功后,通过调用代码的方式手动向MQ确认消息已经收到。当业务处理失败,就可以做一些重试机制,甚至让MQ重新向消费者发送消息都是可以的。
根据异常情况确认acknowledge="auto":该方式是通过抛出异常的类型,来做响应的处理(如重发、确认等布拉不拉布拉)。这种方式比较麻烦。
当消息一旦被消费者接收到,会立刻自动向MQ确认接收,并将响应的message从RabbitMQ消息缓存中移除,但是在实际的业务处理中,会出现消息收到了,但是业务处理出现异常的情况,在自动确认的模式下,该条业务处理失败的message就相当于被丢弃了。如果设置了手动确认,则需要在业务处理完成之后,手动调用channel.basicAck(),手动的签收,如果业务处理失败,则手动调用channel.basicNack()方法拒收,并让MQ重新发送该消息。
如果不做任何关于acknowledge的配置,默认就是自动确认签收的。
生产者端没有变化,能发消息就可以
@Test public void testAck() { // //消费者接收到该消息,解析到true,就模拟调用channel.basicAck确认签收消息 // rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, "boot.test", "test msg send [true]"); //消费者接收到该消息,解析到false,就模拟调用channel.basicNack,拒收消息,让MQ重发 rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, "boot.test", "test msg send [false]"); }
消费者端打开Ack模式
server: port: 2002 spring: rabbitmq: host: 127.0.0.1 port: 5672 username: xxxxxxxxx password: xxxxxxxxx virtual-host: /LeoLee listener: simple: acknowledge-mode: manual #消费者端确认模式:none自动确认 manual手动确认 auto通过抛出异常的类型,来做响应的处理 concurrency: 1 #当前监听的数量 max-concurrency: 5 #最大监听数量 retry: enabled: true #是否支持重试 max-attempts: 4 #最大重试次数,默认为3
消费者端创建一个listener并实现ChannelAwareMessageListener接口(其实也可以不实现该接口,只要@RabbitListener标记的方法,或者@RabbitListener标记的类+@RabbitHandler标记的方法的参数列表有[com.rabbitmq.client.Channel]和[org.springframework.amqp.core.Message]两个参数,都可以)
package com.leolee.rabbitmq.MsgListener; import com.rabbitmq.client.Channel; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener; import org.springframework.stereotype.Component; import java.io.IOException; /** * @ClassName AckListener * @Description: Consumer Ack * 1.设置手动确认签收:acknowledge-mode: manual, retry.enabled: true #是否支持重试 * 2.实现ChannelAwareMessageListener接口,ChannelAwareMessageListener是MessageListener的子接口 * 3.如果消息接收并处理完成,调用channel.basicAck()向MQ确认签收 * 4.如果消息接收但是业务处理失败,调用channel.basicNack()拒收,要求MQ重新发送 * @Author LeoLee * @Date 2020/11/7 * @Version V1.0 **/ @Component public class AckListener implements ChannelAwareMessageListener { @RabbitListener(queues = "boot_queue") @Override public void onMessage(Message message, Channel channel) throws Exception { Thread.sleep(1000); boolean tag = new String(message.getBody()).contains("true"); System.out.println("接收到msg:" + new String(message.getBody())); //获取mes deliveryTag long deliveryTag = message.getMessageProperties().getDeliveryTag(); try { if (tag) { System.out.println("业务处理成功"); //手动签收 /* * deliveryTag:the tag from the received {@link com.rabbitmq.client.AMQP.Basic.GetOk} or {@link com.rabbitmq.client.AMQP.Basic.Deliver} * multiple: ture确认本条消息以及之前没有确认的消息,false仅确认本条消息 */ channel.basicAck(deliveryTag, false); } else { //模拟业务处理失败抛出异常 System.out.println("业务处理失败"); throw new IOException("业务处理失败"); } } catch (IOException e) { e.printStackTrace(); /* * deliveryTag:the tag from the received {@link com.rabbitmq.client.AMQP.Basic.GetOk} or {@link com.rabbitmq.client.AMQP.Basic.Deliver} * multiple: ture确认本条消息以及之前没有确认的消息,false仅确认本条消息 * requeue: true该条消息重新返回MQ queue,MQ broker将会重新发送该条消息 */ channel.basicNack(deliveryTag, false, true); //也可以使用channel.basicReject(deliveryTag, requeue),它只能拒收单条消息 //channel.basicReject(deliveryTag, true); } } }
用生产者测试类发送不同的消息给MQ成功接收并手动确认后,MQ队列就删除了该消息的缓存,被拒绝的消息一直发送.
消费端限流
场景
在处理秒杀场景时经常会用到rabbitmq削峰限流作用,假设我们的系统能每秒处理1000个请求,如果有上万个请求同时打进来,会造成服务器的瘫痪。
这时就需要在系统之前加一次处理,将请求发送的MQ中,再让A系统以每秒1000的速率去请求mq服务器。
具体实现
- 配置prefetch数量
- 确认方式设置为手动确认
yml
spring.application.name=springboot_rabbitmq spring.rabbitmq.host=192.168.0.102 spring.rabbitmq.port=5672 spring.rabbitmq.username=admin spring.rabbitmq.password=admin spring.rabbitmq.virtual-host=/ spring.rabbitmq.listener.simple.acknowledge-mode=manual spring.rabbitmq.listener.simple.prefetch=2
消费者代码
@Component @RabbitListener(queuesToDeclare = @Queue(name = "springboot-limit")) public class CurrentlimitCustomer { @RabbitHandler public void receive(String msg, Channel channel, Message message) throws IOException { long deliveryTag = message.getMessageProperties().getDeliveryTag(); try { Thread.sleep(1000*10); System.out.println("=====限流====>"); System.out.println(msg); System.out.println(channel); System.out.println(message); //手动签收[参数1:消息投递序号,参数2:批量签收] channel.basicAck(deliveryTag, true); } catch (Exception e) { } } }
注意:消费者如果不设置 channel.basicAck(deliveryTag, true); 进行消息确认,则永远不用从消息队列中再取消息
生产者代码
@Test public void test09() throws Exception { for (int i = 0; i < 10 ; i++) { rabbitTemplate.convertAndSend("springboot-limit", "限流测试"); } Thread.sleep(1000 * 1000); }
效果图