🔥个人主页: 中草药
🔥专栏:【中间件】企业级中间件剖析
一、消息确认机制
消费者确认机制确保消息被正确处理后才从队列中删除。如果消费者处理失败(如业务异常或宕机),Broker 会重新投递消息。
确认模式
1. 自动确认(autoAck=true
)
-
原理:
消息一旦被消费者接收(无论是否处理成功),立即自动确认并从队列删除。 -
风险:
若消费者在处理消息时崩溃或抛出异常,消息将永久丢失(因为已被确认删除)。 -
适用场景:
非关键业务,允许消息偶尔丢失(如日志采集、监控数据)。
2. 手动确认(autoAck=false
)
-
原理:
消费者在处理消息后,必须显式调用basicAck()
确认消息;若处理失败,调用basicNack()
或basicReject()
拒绝消息。 -
核心方法:
-
basicAck(deliveryTag, multiple)
:确认单条或批量消息。 -
basicNack(deliveryTag, multiple, requeue)
:拒绝消息,可选择是否重新入队。 -
basicReject(deliveryTag, requeue)
:拒绝单条消息(不支持批量)。
-
-
适用场景:
关键业务场景,要求消息必须可靠处理(如订单支付、库存扣减)。
手动确认(Spring Boot 示例)
spring:rabbitmq:listener:simple:acknowledge-mode: manual # 开启手动确认
Spring-AMQP对消息确认机制提供了三种策略
public enum AcknowledgeMode {NONE,MANUAL,AUTO;private AcknowledgeMode() {}public boolean isTransactionAllowed() {return this == AUTO || this == MANUAL;}public boolean isAutoAck() {return this == NONE;}public boolean isManual() {return this == MANUAL;}
}
AcknowledgeMode.NONE
这种模式下,消息一旦投递 给消费者,不管消费者是否成功处理了消息,RabbitMQ 就会自动确认消息,从 RabbitMQ 队列中移除消息。如果消费者处理消息失败,消息可能会丢失。
AcknowledgeMode.AUTO (默认)
这种模式下,消费者在消息处理成功时会自动确认消息,但如果处理过程中抛出了异常,则不会确认消息。
AcknowledgeMode.MANUAL
手动确认模式下,消费者必须在成功处理消息后显式调用 basicAck 方法来确认消息。如果消息未被确认,RabbitMQ 会认为消息尚未被成功处理,并且会在消费者可用时重新投递该消息,这种模式提高了消息处理的可靠性,因为即使消费者处理消息后失败,消息也不会丢失,而是可以被重新处理。
测试Demo
producer
@RestController
@RequestMapping("/producer")
public class ProducerController {@Autowiredprivate RabbitTemplate rabbitTemplate;@RequestMapping("/ack")public String ack(){rabbitTemplate.convertAndSend(Constants.ACK_EXCHANGE,"ack","ack mode test");return "ack mode test";}
}
listener
@Component
public class AckListener {@RabbitListener(queues = Constants.ACK_QUEUE)public void handleMessage(Message message, Channel channel) throws IOException {long deliveryTag = message.getMessageProperties().getDeliveryTag();try{System.out.printf("接收到消息: %s , deliveryTag = %d,\n",new String(message.getBody(),"UTF-8"),message.getMessageProperties().getDeliveryTag());System.out.println("业务逻辑处理1");int n=3/0;System.out.println("业务逻辑处理2");channel.basicAck(deliveryTag,false);}catch (Exception e) {channel.basicNack(deliveryTag,false,true);}}
}
二、持久化
RabbitMQ 的持久化是确保消息在服务器重启或异常崩溃后不丢失的核心机制。它通过将交换机、队列和消息内容写入磁盘来实现数据的持久保存。以下是持久化的详细说明及配置方法:
持久化的三个层级
交换机(Exchange)持久化
作用:确保交换机元数据在服务器重启后仍存在。
配置方式:声明交换机时设置 durable=true
。
代码示例(Spring Boot):
@Bean
public Exchange orderExchange() {return ExchangeBuilder.directExchange("order.exchange").durable(true) // 持久化交换机.build();
}
队列(Queue)持久化
作用:确保队列元数据(如队列名称、绑定关系)和消息的存储位置在重启后保留。
配置方式:声明队列时设置 durable=true
。
代码示例:
@Bean
public Queue orderQueue() {return QueueBuilder.durable("order.queue") // 持久化队列.deadLetterExchange("dlx.exchange") // 可选:绑定死信队列.build();
}
消息(Message)持久化
作用:将消息内容持久化到磁盘,防止服务器断电或重启导致消息丢失。
配置方式:发送消息时设置 deliveryMode=2
(持久化模式)。
代码示例:
rabbitTemplate.convertAndSend("order.exchange", "order.routingKey", message, msg -> {msg.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT); // 消息持久化return msg;
});
消息是存储在队列之中的,所以消息持久化需要队列持久化+消息持久化 若仅仅是单独设置某一方的持久化,在MQ重启之后,消息仍然会发生丢失
三、发送方确认
以上所述内容分别对应三个环节的RabbitMQ的可靠性保证,前文已经对发布确认模式有了一个详细的介绍,这里我们进行补充
【RabbitMQ】RabbitMQ的核心概念与七大工作模式_mq的几种模式-CSDN博客
针对确保消息可靠发送到服务器,
a. 通过事务机制实现
b. 通过发送方确认 (publisher confirm) 机制实现
事务机制比较消耗性能,在实际工作中使用也不多,这里主要介绍 confirm 机制来实现发送方的确认。RabbitMQ为我们提供了两个方式来控制消息的可靠性传递
1、confirm确认模式
2、return退回模式
两种方法并不互斥,可以单独使用,也可以结合使用
Confirm确认模式
Producer 在发送消息的时候,对发送端设置一个 ConfirmCallback 的监听,无论消息是否到达 Exchange,这个监听都会被执行,如果 Exchange 成功收到,ACK (Acknowledge character,确认字符) 为 true,如果没收到消息,ACK 就为 false.
配置
测试代码
RabbitMQTemplate
@Configuration
public class RabbitTemplateConfig {@Bean(name = "rabbitTemplate")public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {return new RabbitTemplate(connectionFactory);}@Bean(name = "confirmRabbitTemplate")public RabbitTemplate confirmRabbitTemplate(ConnectionFactory connectionFactory) {//设置回调方法RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {@Overridepublic void confirm(CorrelationData correlationData, boolean b, String s) {System.out.println("执行了confirm方法");if(b){System.out.printf("接收到消息,消息ID:%s \n",correlationData==null?null:correlationData.getId());}else {System.out.printf("未接收到消息的ID:%s \n, cause: %s",correlationData==null?null:correlationData.getId(),s);//相应的业务逻辑处理}}});return rabbitTemplate;}}
@RestController
@RequestMapping("/producer")
public class ProducerController {@Autowiredprivate RabbitTemplate rabbitTemplate;@Autowiredprivate RabbitTemplate confirmRabbitTemplate;@RequestMapping("/pers")public String pers(){Message message=new Message("Persistent".getBytes(),new MessageProperties());message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT);rabbitTemplate.convertAndSend(Constants.PERS_EXCHANGE,"pers","pers test");return "pers mode test";}@RequestMapping("/confirm")public String confirm(){CorrelationData correlationData=new CorrelationData("1");// rabbitTemplate.convertAndSend(Constants.CONFIRM_EXCHANGE,"confirm","confirm test",correlationData);//错误演示 Constants.CONFIRM_EXCHANGE+"1" 交换机不存在confirmRabbitTemplate.convertAndSend(Constants.CONFIRM_EXCHANGE+"1","confirm","confirm test",correlationData);return "confirm test";}
}
注意,回调方法只能设置一次,因此要使用配置的方法,如果将回调方法写在controller的方法内部,会致使出现以下问题
1、这种方式设置的confirmcallback会影响所有使用RabbitTemplate的方法
2、重复调用接口时,会提示错误
return退回模式
消息到达 Exchange 之后,会根据路由规则匹配,把消息放入 Queue 中。Exchange 到 Queue 的过程,如果一条消息无法被任何队列消费 (即没有队列与消息的路由键匹配或队列不存在等),可以选择把消息退回给发送者。消息退回给发送者时,我们可以设置一个返回回调方法,对消息进行处理。
首先同样进行配置(如图前文confirm一致)
设置返回的逻辑
@Configuration
public class RabbitTemplateConfig {@Bean(name = "rabbitTemplate")public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {return new RabbitTemplate(connectionFactory);}@Bean(name = "confirmRabbitTemplate")public RabbitTemplate confirmRabbitTemplate(ConnectionFactory connectionFactory) {//设置回调方法RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {@Overridepublic void confirm(CorrelationData correlationData, boolean b, String s) {System.out.println("执行了confirm方法");if(b){System.out.printf("接收到消息,消息ID:%s \n",correlationData==null?null:correlationData.getId());}else {System.out.printf("未接收到消息的ID:%s \n, cause: %s",correlationData==null?null:correlationData.getId(),s);//相应的业务逻辑处理}}});// rabbitTemplate.setReturnsCallback(returnedMessage -> {
// System.out.println("消息退回");
// });rabbitTemplate.setMandatory(true);//强制性rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {@Overridepublic void returnedMessage(ReturnedMessage returnedMessage) {System.out.println("消息退回"+returnedMessage);}});return rabbitTemplate;}
}
RabbitTemplate的setMandatory为true时,当有一条消息无法被任何队列消费,RabbitMQ应该会将消息返回该发送者,此时的ReturnCallBack方法就会被处罚
回调函数中有一个参数:ReturnedMessage
public class ReturnedMessage {//返回的消息对象,包含了消息体和消息属性private final Message message;//由Broker提供的回复码, 表⽰消息⽆法路由的原因. 通常是⼀个数字代码,每个数字代表不同
的含义. private final int replyCode;//⼀个⽂本字符串, 提供了⽆法路由消息的额外信息或错误描述.private final String replyText;//消息被发送到的交换机名称private final String exchange;//消息的路由键,即发送消息时指定的键private final String routingKey;
}
重试机制
在消息传递过程中,可能会遇到各种问题,如网络故障,服务不可用,资源不足等,这些问题可能导致消息处理失败,为了解决这些问题,RabbitMQ 提供了重试机制,允许消息在处理失败后重新发送.但如果是程序代码逻辑引起的错误,那么多次重试也是没有用的,可以设置重试次数。
注意:此重试策略的配置只有在acknowledge-mode 为 auto 时,才会生效
设置Listener消费信息
@Component
public class RetryListener {@RabbitListener(queues = Constants.RETRY_QUEUE)public void handleMessage(Message message) {System.out.printf("["+Constants.RETRY_QUEUE+"]接收到消息: %s ,deliverTag: %s \n",new String(message.getBody()),message.getMessageProperties().getDeliveryTag());int num=3/0;System.out.println("业务处理完成");}//手动确认
// @RabbitListener(queues = Constants.RETRY_QUEUE)
// public void handleMessage(Message message,Channel channel) throws Exception {
// System.out.printf("["+Constants.RETRY_QUEUE+"]接收到消息: %s ,deliverTag: %s \n",new String(message.getBody()),message.getMessageProperties().getDeliveryTag());
// try{
// int num=3/0;
// System.out.println("业务处理完成");
// channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);//是否批量 false
// }catch (Exception e){
// channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,true);//b-是否批量 b1-是否重新入队
// }
// }
}
此时会按照我们重试策略的配置来进行重试
使用重试机制时需要注意:
1.自动确认模式下: 程序逻辑异常,多次重试还是失败,消息就会被自动确认,那么消息就丢失了
2.手动确认模式下: 程序逻辑异常,多次重试消息依然处理失败,无法被确认,就一直是unacked的状态,导致消息积压
如何保证RabbitMQ消息的可靠传输
消息可能丢失的场景以及解决方案
生产者将消息发送到 broker 失败
a. 可能原因:网络问题等
b. 解决办法:发送方确认 - confirm 确认模式
消息在交换机中无法路由到指定队列
a. 可能原因:代码或者配置层面错误,导致消息路由失败
b. 解决办法:发送方确认 - return 模式
消息队列自身数据丢失
a. 可能原因:消息到达 RabbitMQ 之后,RabbitMQ Server 宕机导致消息丢失.
b. 解决办法:持久性,开启 RabbitMQ 持久化,就是消息写入之后会持久化到磁盘,如果 RabbitMQ 挂了,恢复之后会自动读取之前存储的数据.(极端情况下,RabbitMQ 还未持久化就挂了,可能导致少量数据丢失,这个概率极低,也可以通过集群的方式提高可靠性)
消费者异常,导致消息丢失
a. 可能原因:消息到达消费者,还没来得及消费,消费者宕机。消费者逻辑有问题.
b. 解决办法:消息确认,RabbitMQ 提供了消费者应答机制来使 RabbitMQ 能够感知到消费者是否消费成功消息。默认情况下消费者应答机制是自动应答的,可以开启手动确认,当消费者确认消费成功后才会删除消息,从而避免消息丢失。除此之外,也可以配置重试机制 , 当消息消费异常时,通过消息重试确保消息的可靠性
四、TTL
TTL(Time to live,过期时间),RabbitMQ可对消息和队列设置TTL,当消息打到大存活时间之后,还没有被消费就会被自动清除。
设置TTL的方法有很多种,这里不进行一一赘述
设置消息TTL
目前有两种方法可以设置消息的TTL.
一是设置队列的TTL,队列中所有消息都有相同的过期时间,二是对消息本身进行单独设置,每条消息的TTL可以不同.如果两种方法一起使用,则消息的TTL以两者之间较小的那个数值为准.
@RestController
@RequestMapping("/producer")
public class ProducerController {@Autowiredprivate RabbitTemplate rabbitTemplate;@RequestMapping("/ttl")public String ttl(){System.out.println("ttl ...");// MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
// @Override
// public Message postProcessMessage(Message message) throws AmqpException {
// message.getMessageProperties().setExpiration("10000"); //单位毫秒
// return message;
// }
// };//lambdarabbitTemplate.convertAndSend(Constants.TTL_EXCHANGE,"ttl","ttl test",message -> {message.getMessageProperties().setExpiration("10000"); //单位毫秒return message;});return "ttl test";}}
即可通过队列观察到消息
设置队列TTL
@Configuration
public class RabbitMQConfig {//TTL//未设置TTL的队列@Bean("ttlQueue")public Queue ttlQueue() {return QueueBuilder.durable(Constants.TTL_QUEUE).build();}//设置ttl的队列@Bean("ttlQueue2")public Queue ttlQueue2() {return QueueBuilder.durable(Constants.TTL_QUEUE2).ttl(20000).build(); //设置队列的ttl为20s}@Bean("ttlExchange")public DirectExchange ttlExchange() {return ExchangeBuilder.directExchange(Constants.TTL_EXCHANGE).build();}@Bean("ttlBinding")public Binding ttlBinding(@Qualifier("ttlQueue") Queue ttlQueue,@Qualifier("ttlExchange") Exchange ttlExchange) {return BindingBuilder.bind(ttlQueue).to(ttlExchange).with("ttl").noargs();}@Bean("ttlBinding2")public Binding ttlBinding2(@Qualifier("ttlQueue2") Queue ttlQueue,@Qualifier("ttlExchange") Exchange ttlExchange) {return BindingBuilder.bind(ttlQueue).to(ttlExchange).with("ttl").noargs();}
}
为什么这两种方法处理的方式不一样?
因为设置队列过期时间,队列中已过期的消息肯定在队列头部,RabbitMO只要定期从队头开始扫描是否有过期的消息即可.
而设置消息TTL的方式,每条消息的过期时间不同,如果要删除所有过期消息需要扫描整个队列,所以不如等到此消息即将被消费时再判定是否过期,如果过期再进行删除即可.
五、死信队列
死信(Dead Letter)是消息队列中的一种特殊消息,它指的是那些无法被正常消费或处理的消息,死信队列用于存储这些死信消息
消息变为死信一般有以下几种情况
消息过期:消息在队列中存活的时间超过了设定的TTL
消息被拒绝:消费者在处理消息时,可能因为消息内容错误,处理逻辑异常等原因拒绝处理该消息(Basic.Reject/Basic.Nack).如果拒绝时指定不重新入队(requeue=false),消息也会成为死信
队列达到最大长度:当队列达到最大长度,无法再容纳新的消息时,新来的消息会被处理为死信.
代码示例
@Configuration
public class DLConfig {//正常@Bean("normalQueue")public Queue normalQueue(){return QueueBuilder.durable(Constants.NORMAL_QUEUE).deadLetterExchange(Constants.DL_EXCHANGE).deadLetterRoutingKey("dlx").maxLength(10).ttl(10000).build();}@Bean("normalExchange")public DirectExchange normalExchange(){return ExchangeBuilder.directExchange(Constants.NORMAL_EXCHANGE).build();}@Bean("bindingNormal")public Binding bindingNormal(@Qualifier("normalQueue") Queue normalQueue,@Qualifier("normalExchange") DirectExchange normalExchange){return BindingBuilder.bind(normalQueue).to(normalExchange).with("normal");}//死信@Bean("dlQueue")public Queue dlQueue(){return QueueBuilder.durable(Constants.DL_QUEUE).build();}@Bean("dlExchange")public DirectExchange dlExchange(){return ExchangeBuilder.directExchange(Constants.DL_EXCHANGE).build();}@Bean("bindingDl")public Binding bindingDl(@Qualifier("dlQueue") Queue dlQueue,@Qualifier("dlExchange") DirectExchange dlExchange){return BindingBuilder.bind(dlQueue).to(dlExchange).with("dlx");}
}
Listener
@Component
public class DLListener {@RabbitListener(queues = Constants.NORMAL_QUEUE)public void handleMessage(Message message, Channel channel) throws IOException {long deliveryTag = message.getMessageProperties().getDeliveryTag();try{System.out.printf("[normal.queue]接收到消息: %s , deliveryTag = %d,\n",new String(message.getBody(),"UTF-8"),message.getMessageProperties().getDeliveryTag());System.out.println("业务逻辑处理1");int n=3/0;System.out.println("业务逻辑处理2");channel.basicAck(deliveryTag,false);}catch (Exception e) {channel.basicNack(deliveryTag,false,false);}}@RabbitListener(queues = Constants.DL_QUEUE)public void dlHandleMessage(Message message, Channel channel) throws IOException {System.out.printf("[dl.queue]接收到消息: %s , deliveryTag = %d,\n",new String(message.getBody(),"UTF-8"),message.getMessageProperties().getDeliveryTag());}}
controller
@RestController
@RequestMapping("/producer")
public class ProducerController {@Autowiredprivate RabbitTemplate rabbitTemplate;@RequestMapping("/dl")public String dl(){System.out.println("dl ...");//发送普通消息//rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE,"normal","dl test");//测试队列长度for (int i = 0; i < 11; i++) {rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE,"normal","dl test "+i);}return "dl test";}}
即可进行测试
死信队列的应用场景
对于RabbitMQ来说,死信队列是一个非常有用的特性,它可以处理异常情况下,消息不能够被消费者正确消费而被置入死信队列中的情况,应用程序可以通过消费这个死信队列中的内容来分析当时所遇到的异常情况,进而可以改善和优化系统,
比如: 用户支付订单之后,支付系统会给订单系统返回当前订单的支付状态为了保证支付信息不丢失,需要使用到死信队列机制,当消息消费异常时,将消息投入到死信队列中,由订单系统的其他消费者来监听这个队列,并对数据进行处理(比如发送工单等,进行人工确认).场景的应用场景还有:
消息重试:将死信消息重新发送到原队列或另一个队列进行重试处理消息丢弃:直接丢弃这些无法处理的消息,以避免它们占用系统资源
日志收集:将死信消息作为日志收集起来,用于后续分析和问题定位
抛弃时间的人,时间也抛弃他。——莎士比亚
🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀
以上,就是本期的全部内容啦,若有错误疏忽希望各位大佬及时指出💐
制作不易,希望能对各位提供微小的帮助,可否留下你免费的赞呢🌸