RabbitMQ实战篇:消息确认之消费者局部确认

本文深入探讨RabbitMQ中的消息确认(ACK)机制,包括手动与自动确认的区别,如何在Spring环境中配置和使用ACK,以及如何通过ACK避免消息丢失。通过代码示例展示如何在消费者端确认消息接收,确保消息处理的可靠性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

消息通过 ACK 确认是否被正确接收,每个 Message 都要被确认(acknowledged),可以手动去 ACK 或自动 ACK
自动确认会在消息发送给消费者后立即确认,但存在丢失消息的可能,如果消费端消费逻辑抛出异常,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息。

如果消息已经被处理,但后续代码抛出异常,使用 Spring 进行管理的话消费端业务逻辑会进行回滚,这也同样造成了实际意义的消息丢失。

如果手动确认则当消费者调用 ack、nack、reject 几种方法进行确认,手动确认可以在业务失败后进行一些操作,如果消息未被 ACK 则会发送到下一个消费者。

如果某个服务忘记 ACK 了,则 RabbitMQ 不会再发送数据给它,因为 RabbitMQ 认为该服务的处理能力有限
ACK 机制还可以起到限流作用,比如在接收到某条消息时休眠几秒钟。

下面我们代码演示:

配置类:

package com.lwl.rabbitmq.config;

import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;

public class RabbitConfig {
    
    @Value("${spring.rabbitmq.host}")
    private String addresses;
    
    @Value("${spring.rabbitmq.port}")
    private String port;

    @Value("${spring.rabbitmq.username}")
    private String username;

    @Value("${spring.rabbitmq.password}")
    private String password;

    @Value("${spring.rabbitmq.virtual-host}")
    private String virtualHost;

    @Value("${spring.rabbitmq.publisher-confirms}")
    private boolean publisherConfirms;
    
    @Value("${spring.rabbitmq.publisher-returns}")
    private boolean publisherReturns;
    
    @Bean
    public ConnectionFactory connectionFactory() {

        CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
        connectionFactory.setAddresses(addresses+":"+port);
        connectionFactory.setUsername(username);
        connectionFactory.setPassword(password);
        connectionFactory.setVirtualHost(virtualHost);
        /** 如果要进行消息回调,则这里必须要设置为true */
        connectionFactory.setPublisherConfirms(publisherConfirms);
        connectionFactory.setPublisherReturns(publisherReturns);
        return connectionFactory;
    }
    
    @Bean
    /** 因为要设置回调类,所以应是prototype类型,如果是singleton类型,则回调类为最后一次设置 */
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public RabbitTemplate rabbitTemplatenew() {
        RabbitTemplate template = new RabbitTemplate(connectionFactory());
        return template;
    }

}
package com.lwl.rabbitmq.config;


import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.lwl.rabbitmq.constant.Constants;

/**
 * 发送消息 配置发送消息的队列queue
 * @author lwl
 * @create 2018年8月10日 下午2:37:38
 * @version 1.0
 */
@Configuration
public class SendMessageConfig {

    @Bean
    public Queue topicQueue() {
    	return new Queue(Constants.TOPIC_QUEUE);
    }
    
    @Bean
    TopicExchange exchange() {
        return new TopicExchange(Constants.TOPIC_NAME);
    }
    
    /**
     * 使用主题交换机, 
     *  	将队列Constants.TOPIC_QUEUE与exchange绑定,binding_key为topic.queue.key,就是完全匹配
     * @param topicQueue
     * @param exchange
     * @return
     * @author lwl
     * @create 2019年6月14日 上午10:51:21
     */
    @Bean
    Binding bindingExchangeMessage(Queue topicQueue, TopicExchange  exchange) {
        return BindingBuilder.bind(topicQueue).to(exchange).with(Constants.ROUTING_KEY);
    }
    
    
    
}

# 配置rabbitmq
spring: 
      rabbitmq:
        host: 192.168.3.66
        port: 5672
        username: lwl
        password: 123456
        publisher-confirms: true
        publisher-returns: true
        virtual-host: /
        listener: 
            simple:
                acknowledge-mode: manual

生产者:

package com.lwl.rabbitmq.producer;

import java.util.UUID;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;

import com.lwl.rabbitmq.constant.Constants;

/**
 * 生成者
 * @author lwl
 * @create 2019年6月14日 上午10:56:41
 * @version 1.0
 */
@Component
public class AckProducer implements RabbitTemplate.ConfirmCallback , RabbitTemplate.ReturnCallback{

	@Autowired
    private RabbitTemplate rabbitTemplatenew;

    /**
     * 使用主题交换机
     * @param message
     * @author lwl
     * @create 2019年6月14日 上午10:54:54
     */
    public void send(Object message){
    	rabbitTemplatenew.setConfirmCallback(this);
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());  
        System.out.println();
        System.out.println("callbackSender UUID: " + correlationData.getId());  
        System.out.println();
        this.rabbitTemplatenew.convertAndSend(Constants.TOPIC_NAME,Constants.ROUTING_KEY, message, correlationData);  
    }
    /**
     * 
     * 通过实现 ConfirmCallback 接口,消息发送到 Broker 后触发回调,确认消息是否到达 Broker 服务器,也就是只确认是否正确到达 Exchange 中
     * @param correlationData
     * @param ack
     * @param cause
     * @author lwl
     * @create 2019年6月17日 上午9:46:59
     */
	@Override
	public void confirm(CorrelationData correlationData, boolean ack, String cause) {
		//
		System.out.println();
		System.out.println();
		 System.out.println("callbakck confirm: " + correlationData.getId());
		 System.out.println();
		 System.out.println();
	}
	
	/**
	 * 通过实现 ReturnCallback 接口,启动消息失败返回,比如路由不到队列时触发回调
	 * @param message
	 * @param replyCode
	 * @param replyText
	 * @param exchange
	 * @param routingKey
	 * @author lwl
	 * @create 2019年6月17日 上午10:01:47
	 */
	@Override
	public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
		System.out.println("消息主体 message : "+message);
        System.out.println("消息主体 message : "+replyCode);
        System.out.println("描述:"+replyText);
        System.out.println("消息使用的交换器 exchange : "+exchange);
        System.out.println("消息使用的路由键 routing : "+routingKey);		
	}
    
    
}

消费者:

package com.lwl.rabbitmq.consumer;

import java.io.IOException;

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
import org.springframework.amqp.support.AmqpHeaders;

import com.lwl.rabbitmq.constant.Constants;
import com.rabbitmq.client.Channel;

/**
 * 消费者
 * @author lwl
 * @create 2019年6月14日 上午10:57:11
 * @version 1.0
 */
@Component
@RabbitListener(queues = Constants.TOPIC_QUEUE)
public class TopicAckConsumer {

	
	@RabbitHandler
	public void processMessage2(String message,Channel channel,@Header(AmqpHeaders.DELIVERY_TAG) long tag) {
		System.out.println("-----------------------客户端  1  收到数据 -----------------------");
		System.out.println(message);
		System.out.println();
	    try {
	    	/**
	    	 * 需要注意的 basicAck 方法需要传递两个参数
					deliveryTag(唯一标识 ID):当一个消费者向 RabbitMQ 注册后,会建立起一个 Channel ,
						RabbitMQ 会用 basic.deliver 方法向消费者推送消息,这个方法携带了一个 delivery tag, 
							它代表了 RabbitMQ 向该 Channel 投递的这条消息的唯一标识 ID,是一个单调递增的正整数,delivery tag 的范围仅限于 Channel
					
					multiple:为了减少网络流量,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息

	    	 */
	        channel.basicAck(tag,false);            // 确认消息
	    	
	    } catch (IOException e) {
	        e.printStackTrace();
	    }
	}
}
package com.lwl.rabbitmq.consumer;

import java.io.IOException;
import java.util.Map;

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;
import org.springframework.amqp.support.AmqpHeaders;

import com.lwl.rabbitmq.constant.Constants;
import com.rabbitmq.client.Channel;

/**
 * 消费者
 * @author lwl
 * @create 2019年6月14日 上午10:57:11
 * @version 1.0
 */
@Component
@RabbitListener(queues = Constants.TOPIC_QUEUE)
public class NackTopicAckConsumer {

	
	/**
	 * 消费者获取消息时检查到头部包含 error 则 nack 消息
	 * @param message
	 * @param channel
	 * @param map
	 * @author lwl
	 * @create 2019年6月17日 上午10:52:54
	 */
	@RabbitHandler
	public void processMessage2(String message, Channel channel,@Headers Map<String,Object> map) {
		System.out.println("-----------------------客户端  nack  收到数据 -----------------------");
	    System.out.println(message);
	    if (map.get("error")!= null){
	        System.out.println("错误的消息");
	        try {
	        	//此时控制台重复打印,说明该消息被 nack 后一直重新入队列然后一直重新消费
	            channel.basicNack((Long)map.get(AmqpHeaders.DELIVERY_TAG),false,true);      //否认消息
	            
	            //也可以拒绝该消息,消息会被丢弃,不会重回队列
//	            channel.basicReject((Long)map.get(AmqpHeaders.DELIVERY_TAG),false);        //拒绝消息

	            
	        } catch (IOException e) {
	            e.printStackTrace();
	        }
	    }
	    try {
	        channel.basicAck((Long)map.get(AmqpHeaders.DELIVERY_TAG),false);            //确认消息
	        
//	        channel.basicNack((Long)map.get(AmqpHeaders.DELIVERY_TAG),false,true);      //否认消息
	        
//	        channel.basicReject((Long)map.get(AmqpHeaders.DELIVERY_TAG),false);        //拒绝消息
	        
	    } catch (IOException e) {
	        e.printStackTrace();
	    }
	}
}

测试用例:

	/**
	 * 把(NackTopicAckConsumer 和 TopicAckConsumer ) 2个消费者注释掉,保证数据发送到队列中
	 * @author lwl
	 * @create 2019年6月29日 上午9:47:39
	 */
	@Test
	public void send() {
		String message = " message with ack C2 ";
		ackProducer.send(message );
	}

我们看到消息队列中有2条数据,那么我们先开启一个消费者NackTopicAckConsumer, 但是我们将确认回复注释掉,看看运行结果

package com.lwl.rabbitmq.consumer;

import java.io.IOException;
import java.util.Map;

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;
import org.springframework.amqp.support.AmqpHeaders;

import com.lwl.rabbitmq.constant.Constants;
import com.rabbitmq.client.Channel;

/**
 * 消费者
 * @author lwl
 * @create 2019年6月14日 上午10:57:11
 * @version 1.0
 */
@Component
@RabbitListener(queues = Constants.TOPIC_QUEUE)
public class NackTopicAckConsumer {

	
	/**
	 * 消费者获取消息时检查到头部包含 error 则 nack 消息
	 * @param message
	 * @param channel
	 * @param map
	 * @author lwl
	 * @create 2019年6月17日 上午10:52:54
	 */
	@RabbitHandler
	public void processMessage2(String message, Channel channel,@Headers Map<String,Object> map) {
		System.out.println("-----------------------客户端  nack  收到数据 -----------------------");
	    System.out.println(message);
	    if (map.get("error")!= null){
	        System.out.println("错误的消息");
	        try {
	        	//此时控制台重复打印,说明该消息被 nack 后一直重新入队列然后一直重新消费
	            channel.basicNack((Long)map.get(AmqpHeaders.DELIVERY_TAG),false,true);      //否认消息
	            
	            //也可以拒绝该消息,消息会被丢弃,不会重回队列
//	            channel.basicReject((Long)map.get(AmqpHeaders.DELIVERY_TAG),false);        //拒绝消息

	            
	        } catch (IOException e) {
	            e.printStackTrace();
	        }
	    }
//	    try {
//	        channel.basicAck((Long)map.get(AmqpHeaders.DELIVERY_TAG),false);            //确认消息
//	        
	        channel.basicNack((Long)map.get(AmqpHeaders.DELIVERY_TAG),false,true);      //否认消息
//	        
	        channel.basicReject((Long)map.get(AmqpHeaders.DELIVERY_TAG),false);        //拒绝消息
//	        
//	    } catch (IOException e) {
//	        e.printStackTrace();
//	    }
	}
}
	/**
	 * 开启消费者 NackTopicAckConsumer , 但是我们将确认回复注释掉,看看运行结果
	 * @author lwl
	 * @create 2019年6月29日 上午9:50:26
	 */
	@Test
	public void send2() {
	}
-----------------------客户端  nack  收到数据 -----------------------
 message with ack C2 
-----------------------客户端  nack  收到数据 -----------------------
 message with ack C3 

我们看到运行结果是消费队列中的2个消息,但是真的是这样子嘛?

看队列中的数据还是在的,所以虽然他消费了消息,但是并没有给确认信息,队列的中数据都没有被删除,那么我们在把确认信息都打开看看。

/**
	 * 开启消费者 NackTopicAckConsumer , 我们将确认回复注释掉去掉,看看运行结果
	 * @author lwl
	 * @create 2019年6月29日 上午9:50:26
	 */
	@Test
	public void send3() {
	}
package com.lwl.rabbitmq.consumer;

import java.io.IOException;
import java.util.Map;

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;
import org.springframework.amqp.support.AmqpHeaders;

import com.lwl.rabbitmq.constant.Constants;
import com.rabbitmq.client.Channel;

/**
 * 消费者
 * @author lwl
 * @create 2019年6月14日 上午10:57:11
 * @version 1.0
 */
@Component
@RabbitListener(queues = Constants.TOPIC_QUEUE)
public class NackTopicAckConsumer {

	
	/**
	 * 消费者获取消息时检查到头部包含 error 则 nack 消息
	 * @param message
	 * @param channel
	 * @param map
	 * @author lwl
	 * @create 2019年6月17日 上午10:52:54
	 */
	@RabbitHandler
	public void processMessage2(String message, Channel channel,@Headers Map<String,Object> map) {
		System.out.println("-----------------------客户端  nack  收到数据 -----------------------");
	    System.out.println(message);
	    if (map.get("error")!= null){
	        System.out.println("错误的消息");
	        try {
	        	//此时控制台重复打印,说明该消息被 nack 后一直重新入队列然后一直重新消费
	            channel.basicNack((Long)map.get(AmqpHeaders.DELIVERY_TAG),false,true);      //否认消息
	            
	            //也可以拒绝该消息,消息会被丢弃,不会重回队列
//	            channel.basicReject((Long)map.get(AmqpHeaders.DELIVERY_TAG),false);        //拒绝消息

	            
	        } catch (IOException e) {
	            e.printStackTrace();
	        }
	    }
	    try {
	        channel.basicAck((Long)map.get(AmqpHeaders.DELIVERY_TAG),false);            //确认消息
	        
//	        channel.basicNack((Long)map.get(AmqpHeaders.DELIVERY_TAG),false,true);      //否认消息
	        
//	        channel.basicReject((Long)map.get(AmqpHeaders.DELIVERY_TAG),false);        //拒绝消息
	        
	    } catch (IOException e) {
	        e.printStackTrace();
	    }
	}
}

运行结果:

-----------------------客户端  nack  收到数据 -----------------------
 message with ack C2 
-----------------------客户端  nack  收到数据 -----------------------
 message with ack C3 

看,消息都被消费掉了,而且也从队列中删除了。

那么剩下的几种情况,你们可以自己尝试看看。

  channel.basicAck((Long)map.get(AmqpHeaders.DELIVERY_TAG),false);            //确认消息
	        
//	 channel.basicNack((Long)map.get(AmqpHeaders.DELIVERY_TAG),false,true);      //否认消息
	        
//	 channel.basicReject((Long)map.get(AmqpHeaders.DELIVERY_TAG),false);        //拒绝消息

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值