为了保证消息从队列可靠的达到消费者,RabbitMQ 提供了消息确认机制(Message Acknowledgement)。消费者在订阅队列时,可以指定 autoAck 参数,当 autoAck 参数等于 false 时,RabbitMQ 会等待消费者显式地回复确认信号后才从内存(或者磁盘)中移除消息(实际上是先打上删除标记,之后在删除)。当 autoAck 参数等于 true 时,RabbitMQ 会自动把发送出去的消息置为确认,然后从内存(或者磁盘)中删除,而不管消费者是否真正地消费到了这些消息。
采用消息确认机制后,只要设置 autoAck 参数为 false,消费者就有足够的时间处理消息(任务),不用担心处理消息过程中消费者进程挂掉后消息丢失的问题,因为 RabbitMQ 会一直等待持有消息直到消费者显式调用 Basic.Ack 命令为止。
当autoAck 参数为 false 时,对于 RabbitMQ 服务器端而言,队列中的消息分成了两部分:一部分是等待投递给消费者的消息;一部分是已经投递给消费者,但是还没有收到消费者确认信号的消息。如果 RabbitMQ 服务器端一直没有收到消费者的确认信号,并且消费此消息的消费者已经断开连接,则服务器端会安排该消息重新进入队列,等待投递给下一个消费者(也可能还是原来的那个消费者)。
RabbitMQ 消息确认机制分为两大类:发送方确认、接收方确认。
其中发送方确认又分为:生产者到交换器到确认、交换器到队列的确认。如下图:
确认消息是否到达交换机
确认消息是否从交换机发送到队列
spring:
application:
name: rabbitmq-server
#RabbitMQ
rabbitmq:
#ip
host: 192.168.17.128
#用户名
username: rabbitmq
#密码
password: rabbitmq
#端口
port: 5673
#虚拟主机
virtual-host: rabbitmq
#开启确认模式
publisher-confirm-type: correlated
#开始回退模式
publisher-returns: true
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@Slf4j
public class ConfirmAndReturn {
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
RabbitTemplate rabbitTemplate=new RabbitTemplate(connectionFactory);
//设置确认模式,确认消息是否到达交换机
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
*
* @param correlationData 相关配置信息
* @param ack 是否发送成功
* @param cause 错误原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
log.info("消息到达交换机....");
}else {
log.error("消息没有到达交换机,原因:{}",cause);
}
}
});
//设置回退回退,确认消息是否从交换机到达队列
//默认没有达到队列,消息直接丢弃,setMandatory(true),则将消息返给broker
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnsCallback(returned -> {
log.error("消息没有到达队列.....");
//消息对象
Message message = returned.getMessage();
System.out.println("message = " + message);
//错误码
int replyCode = returned.getReplyCode();
System.out.println("replyCode = " + replyCode);
//错误信息
String replyText = returned.getReplyText();
System.out.println("replyText = " + replyText);
//交换机名称
String exchange = returned.getExchange();
System.out.println("exchange = " + exchange);
//路由键
String routingKey = returned.getRoutingKey();
System.out.println("routingKey = " + routingKey);
});
return rabbitTemplate;
}
@Bean
public Queue confirmQueue(){
return QueueBuilder.durable("confirm-queue").build();
}
@Bean
public DirectExchange confirmExchange(){
return ExchangeBuilder.directExchange("confirm-exchange").build();
}
@Bean
public Binding confirmBinding(){
return BindingBuilder.bind(confirmQueue()).to(confirmExchange()).with("info");
}
@Bean
public RabbitAdmin confirmRabbitAdmin(ConnectionFactory connectionFactory){
RabbitAdmin rabbitAdmin=new RabbitAdmin(connectionFactory);
rabbitAdmin.declareExchange(confirmExchange());
rabbitAdmin.declareQueue(confirmQueue());
return rabbitAdmin;
}
}
private static SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 组装消息
* @param msg
* @return
*/
private static Map<String, Object> createMsg(Object msg) {
String msgId = UUID.randomUUID().toString().replace("-", "").substring(0, 32);
Map<String,Object> message= Maps.newHashMap();
message.put("sendTime",sdf.format(new Date()));
message.put("msg", msg);
message.put("msgId",msgId);
return message;
}
@GetMapping("confirm")
@ApiOperation("消息确认")
public String confirm(@RequestParam String msg){
Map<String, Object> map = createMsg(msg);
//故意写错交换机名字,正确的是confirm-exchange
rabbitTemplate.convertAndSend("confirm-exchange11","info",map);
return "ok";
}
结果:
2023-04-14 16:50:00.831 ERROR 25452 --- [168.17.128:5673] o.s.a.r.c.CachingConnectionFactory : Shutdown Signal: channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'confirm-exchange11' in vhost 'rabbitmq', class-id=60, method-id=40)
2023-04-14 16:50:06.868 ERROR 25452 --- [nectionFactory2] c.r.producer.amqp.ConfirmAndReturn : 消息没有到达交换机,原因:channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'confirm-exchange11' in vhost 'rabbitmq', class-id=60, method-id=40)
private static SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 组装消息
* @param msg
* @return
*/
private static Map<String, Object> createMsg(Object msg) {
String msgId = UUID.randomUUID().toString().replace("-", "").substring(0, 32);
Map<String,Object> message= Maps.newHashMap();
message.put("sendTime",sdf.format(new Date()));
message.put("msg", msg);
message.put("msgId",msgId);
return message;
}
@GetMapping("confirm")
@ApiOperation("消息确认")
public String confirm(@RequestParam String msg){
Map<String, Object> map = createMsg(msg);
//故意写错路由键,正确的是info
rabbitTemplate.convertAndSend("confirm-exchange","info11",map);
return "ok";
}
结果:
2023-04-14 16:52:43.127 ERROR 11760 --- [nectionFactory1] c.r.producer.amqp.ConfirmAndReturn : 消息没有到达队列.....
message = (Body:'[serialized object]' MessageProperties [headers={}, contentType=application/x-java-serialized-object, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0])
replyCode = 312
replyText = NO_ROUTE
exchange = confirm-exchange
routingKey = info11
2023-04-14 16:52:43.129 INFO 11760 --- [nectionFactory2] c.r.producer.amqp.ConfirmAndReturn : 消息到达交换机....
消息接收确认,消费者接收消息有三种不同的确认模式:
1、AcknowledgeMode.NONE:不确认,这是默认的模式,默认所有消息都被成功消费了,直接从队列删除消息。存在消息被消费过程中由于异常未被成功消费而掉丢失的风险。
2、AcknowledgeMode.AUTO:自动确认,根据消息被消费过程中是否发生异常来发送确认收到消息或拒绝消息的指令到 RabbitMQ 服务。这个确认时机开发人员是不可控的,同样存在消息丢失的风险。
3、AcknowledgeMode.MANUAL:手动确认,开发人员可以根据实际的业务,在合适的时机手动发送确认收到消息或拒绝消息指令到 RabbitMQ 服务,整个过程开发人是可控的。这种模式也是我们要重点介绍的。
spring:
application:
name: rabbitmq-consumer
#RabbitMQ
rabbitmq:
#ip
host: 192.168.17.128
#用户名
username: rabbitmq
#密码
password: rabbitmq
#端口
port: 5673
#虚拟主机
virtual-host: rabbitmq
listener:
simple:
#设置手动签收
acknowledge-mode: manual
direct:
#设置手动签收
acknowledge-mode: manual
/**
* ack
* @param message 消息
* @param c 通道
* @param msg 消息内容
* @throws IOException
*/
//使用queuesToDeclare属性,如果不存在则会创建队列,注:此处声明的队列要和生产者属性保持一致
@RabbitListener(queuesToDeclare = @Queue(value = "confirm-queue"))
public void ack(Message message,Channel c,Map msg) throws IOException {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
MessageProperties properties = message.getMessageProperties();
String routingKey = properties.getReceivedRoutingKey();
log.info("ack收到:{},路由键:{}",msg,routingKey);
//手动回执,不批量签收,回执后才能处理下一批消息
long tag = properties.getDeliveryTag();
c.basicAck(tag,false);
//拒绝签收并重回队列
// c.basicNack(tag,false,true);
}