08:SpringBoot-RabbitMQ消息处理-持久化问题
SpringBoot-RabbitMQ消息处理-持久化问题
目标:解决开发中关于消息丢失和持久化的问题。
步骤1:修改autoDelete的状态即可,false是持久化,true是非持久化
概述:消息丢了怎么办?一般我们进行持久化即可,操作如下:
packagecom.itbooking.springbootrabbitmqdurabledirectconsumber.mq;import org.springframework.amqp.core.ExchangeTypes;import org.springframework.amqp.rabbit.annotation.*;import org.springframework.stereotype.Component;// autoDelete 是否持久化操作。false是持久化,true不持久化,@Component@RabbitListener(bindings =@QueueBinding(value = @Queue(value ="${mq.config.queue.error}",autoDelete = "false"),exchange = @Exchange(value ="${mq.config.exchange}",type = ExchangeTypes.DIRECT),key = "${mq.config.queue.error.routing.key}"))public class ErrorReceiver {@RabbitHandlerpublic void process(String msg) {System.out.println("error----------->接受到的的消息是:" + msg);}}
把上面的autoDelete 改成false,即持久化操作。这样可以防止因为网络故障问题导致消费消息不连续的问题。
注意测试的时候后一定是要:先在rabbitmq管控台先删除,在重新生成在测试,结论是 :
1、这个时候把autoDelete改成true以后,一定关闭连接,队列就直接丢失。
2、如果`autoDelete`是false,就是持久化的队列,这个时候即使你重启了rabbitmq服务,消息依然存在。
测试代码如下:
生产者
springboot-rabbitmq-durable-direct-producer
1:引入依赖
2:配置rabbitmq
server.port=8097spring.application.name=springboot-rabbitmq-durabledirect-producer# rabbitmqspring.rabbitmq.host=192.168.153.173spring.rabbitmq.port=5672spring.rabbitmq.username=adminspring.rabbitmq.password=admin# 设置交换机mq.config.exchange=log.direct
3: 发送消息
packagecom.itbooking.springbootrabbitmqdurabledirectproducer.mq;import org.springframework.amqp.core.AmqpTemplate;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Component;import java.util.Date;@Componentpublic class Sender {@Autowiredprivate AmqpTemplate amqpTemplate;@Value("${mq.config.exchange}")private String exchangeName;public void sendMessage(int i) throwsInterruptedException {String msg = i+":Hello rabbitmq " + newDate();amqpTemplate.convertAndSend(exchangeName,"log.error.routing.key",msg);System.out.println(msg);}}
消费者
springboot-rabbitmq-durable-direct-consumber
1:配置rabbitmq
server.port=8196spring.application.name=springboot-rabbitmq-durabledirect-consumber# rabbitmqspring.rabbitmq.host=192.168.153.173spring.rabbitmq.port=5672spring.rabbitmq.username=adminspring.rabbitmq.password=admin# 设置交换机mq.config.exchange=log.direct# 设置路由键mq.config.queue.info=log.infomq.config.queue.info.routing.key=log.info.routing.keymq.config.queue.error=log.errormq.config.queue.error.routing.key=log.error.routing.key
2:定义消费者类
packagecom.itbooking.springbootrabbitmqdurabledirectconsumber.mq;import org.springframework.amqp.core.ExchangeTypes;import org.springframework.amqp.rabbit.annotation.*;import org.springframework.aop.ThrowsAdvice;import org.springframework.stereotype.Component;// autoDelete 是否持久化操作。false是持久化,true不持久化,@Component@RabbitListener(bindings =@QueueBinding(value = @Queue(value ="${mq.config.queue.error}",autoDelete = "false"),exchange = @Exchange(value ="${mq.config.exchange}",type = ExchangeTypes.DIRECT),key = "${mq.config.queue.error.routing.key}"))public class ErrorReceiver {@RabbitHandlerpublic void process(String msg) {System.out.println("error----------->接受到的的消息是:" + msg);}}
3:启动消费者等待消息消费
测试类
packagecom.itbooking.springbootrabbitmqdurabledirectproducer;importcom.itbooking.springbootrabbitmqdurabledirectproducer.mq.Sender;import org.junit.Test;import org.junit.runner.RunWith;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.test.context.junit4.SpringRunner;@RunWith(SpringRunner.class)@SpringBootTestpublic classSpringbootRabbitmqDurableDirectProducerApplicationTests {@Autowiredprivate Sender sender;@Testpublic void contextLoads() throwsInterruptedException {for (int i = 0; i <200 ; i++) {Thread.sleep(2000);sender.sendMessage(i);}}}
给发送消息休眠200秒,慢慢发送。
这个时候停止消费者接受消息,继续发送消息给消费队列。测试持久化的作用。
如果autodelete是true是临时队列,关闭消费者即可删除队列,就会造成消息丢失,测试的时候务必先删除队列让其重新创建,否则无效。
接受消息关闭之前:
关闭之后在启动:
丢失了消息!!!!
如果设置为false,它继续接着没有消费的消息继续消费。
关闭之前消息消费到7的位置,重启以后如下:
消费出现异常:因为rabbitmq当消费者出现异常的时候,rabbitmq的默认情况是一种:自动确认过程,而报异常以后这自动确认的过程,就没办法确认,没办法确认,它就会把这个消息重新放入到消息队列继续去发送,这样就造成死循环。
开启消息重试即可,如果你单机版,会一直重试自己服务器,如果集群版:切换别的服务器重试。如果重试的次数已经达到限制次数,还是都失败,这个消息直接扔掉。
推荐做法:try/catch + 死信 + 重定向+1重试 + 人工干预短信预警消费出现服务宕机:用持久队列
10:消息如何保障100%的投递成功(重点)-手动ACK和Confifirm确认
消息落库,对消息状态进行打标。做法就是把消息发送MQ一份,同时存储到数据库一份。然后进行消息装填的控制,发送成功1,发送失败0。必须结合应答来完成。对于哪些如果没有发送成功的消息,可以采用定时器进行轮询发送。
存在问题:可能出现分布式定时任务出现消息重发的情况。因为在消息保存进去的时候状态是0,其实这个时候ACK是可以回执,但是定时器恰好在那个时间点运行到了。就出现了消息重发的问题,解决方案:幂等性
生产者1:
配置依赖
<dependency><groupId>org.springframework.bootgroupId><artifactId>spring-boot-starter-amqpartifactId>dependency>
2:配置applicaiton.properties
#服务器server.port=7878#配置rabbitmqspring.rabbitmq.port=5672spring.rabbitmq.host=192.168.153.176spring.rabbitmq.virtual-host=/spring.rabbitmq.username=adminspring.rabbitmq.password=admin# 定义交换机mq.config.exchange=log.ack.direct# 设置路由keymq.config.routekey=red.ack.routing.key# 开启消息发送确认机制,重要!!!#spring.rabbitmq.publisher-confirm-type=correlatedspring.rabbitmq.publisher-confirms=true
3:生产者
package com.icoding.springbootrabbitfanoutproducer.mq;import org.springframework.amqp.core.AmqpTemplate;importorg.springframework.amqp.rabbit.connection.CorrelationData;importorg.springframework.amqp.rabbit.core.RabbitTemplate;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Service;import javax.annotation.PostConstruct;@Servicepublic class Sender {// 初始化一个rabbitmq的实例对象@Autowiredprivate RabbitTemplate rabbitTemplate;@Value("${mq.config.exchange}")private String exchangeName;@Value("${mq.config.routekey}")private String routekey;@PostConstructpublic void setup() {// 消息发送完毕后,则回调此方法,ack代表发送是否成功rabbitTemplate.setConfirmCallback(newRabbitTemplate.ConfirmCallback() {@Overridepublic void confirm(CorrelationDatacorrelationData, boolean ack, String cause) {// 如果ack为true,代表MQ已经准备收到消息。if (!ack) {return;}try {System.out.println("接受确认的数据是:" + correlationData.getId());System.out.println("本地消息保存成功!!!");} catch (Exception ex) {System.out.println("警告:修改本地消息表的状态时出现异常!" + ex.getMessage());}}});}public void sendMessage(int i) throwsInterruptedException {//下单String msg = "{id:10001,price:100}";//amqpTemplate.convertAndSend(exchangeName,"sms.routing.key","direct--订单保存成功,去发送短信和微信.....");rabbitTemplate.convertAndSend(exchangeName,routekey, msg, new CorrelationData("10001"));System.out.println(msg);}}
消费者
1:配置手动确认 applicaiton.properties
#服务器server.port=7879#配置rabbitmqspring.rabbitmq.port=5672spring.rabbitmq.host=192.168.153.176spring.rabbitmq.virtual-host=/spring.rabbitmq.username=adminspring.rabbitmq.password=admin# 定义交换机mq.config.exchange=log.ack.direct# 定义队列三个消费者队列# 红包队列mq.config.queue.red=order.ack.red.log# 设置路由keymq.config.red.routekey=red.ack.routing.key# 解决消息的ACK死循环问题--自动确认spring.rabbitmq.listener.simple.retry.enabled=true# 最多重试几次,但是在实际的生产中如果不重要的可以这样做,spring.rabbitmq.listener.simple.retry.max-attempts=5# 重要,开启手动ack,控制消息在MQ中的删除,重发。spring.rabbitmq.listener.simple.acknowledgemode=MANUAL
2:定义消费者并且手动确认
packagecom.icoding.springbootrabbitfanoutconsumber.mq;import com.rabbitmq.client.Channel;import org.springframework.amqp.core.ExchangeTypes;import org.springframework.amqp.rabbit.annotation.*;importorg.springframework.amqp.rabbit.core.RabbitTemplate;import org.springframework.amqp.support.AmqpHeaders;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.messaging.handler.annotation.Header;import org.springframework.stereotype.Service;@Service// 用RabbitListener来完成队列和交换机的定义和绑定关系以及设置交换机的类型// autoDelete= true代表非持久化 false 持久化@RabbitListener(bindings = @QueueBinding(value =@Queue(value="${mq.config.queue.red}",autoDelete="false"),exchange =@Exchange(value ="${mq.config.exchange}",type = ExchangeTypes.DIRECT),key = "${mq.config.red.routekey}"))public class RedxinReceiver {@AutowiredRabbitTemplate rabbitTemplate;@RabbitHandlerpublic void getMessage(String message, Channelchannel, @Header(AmqpHeaders.DELIVERY_TAG) long tag){try {System.out.println("red------接收到订单的消息了,内容是:" + message);//参数1:deliveryTag(唯一标识 ID):当一个消费者向 RabbitMQ 注册后,会建立起一个 Channel ,RabbitMQ 会用basic.deliver 方法向消费者推送消息,这个方法携带了一个delivery tag, 它代表了 RabbitMQ 向该 Channel 投递的这条消息的唯一标识 ID,是一个单调递增的正整数,delivery tag 的范围仅限于 Channel//参数2:multiple:为了减少网络流量,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息channel.basicAck(tag,false);}catch (Exception ex) {ex.printStackTrace();}}}
测试类
SpringbootRabbitDirectProducerAckApplicationTests.java
package com.icoding.springbootrabbitfanoutproducer;importcom.icoding.springbootrabbitfanoutproducer.mq.Sender;import org.junit.jupiter.api.Test;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;@SpringBootTestclassSpringbootRabbitDirectProducerAckApplicationTests {@Autowiredprivate Sender sender;@Testvoid contextLoads() throws Exception{sender.sendMessage(1);}}
运行结果:消费者消费消息
red------接收到订单的消息了,内容是:{id:10001,price:100}
生产者回执消息如下:
{id:10001,price:100}接受确认的数据是:10001本地消息保存成功!!!
下节课预告:SpringBoot-整合RabbitMQ常见问题以及总结
记得点个在看,或者转发朋友圈哦!