RabbitMQ 入门系列4 -- 消息确认机制

1 问题引入

在使用 RabbitMQ 的时候,我们可能会遇到这样一个问题,在生产者发送消息之后,并不知道消息是否已经到达了服务器,这对于生产者来说是一个谜。默认情况下,生产者不会收到任何响应。那么,如果我们想要了解消息的去向,那我们应该怎么做呢?于是,RabbitMQ 的消息确认机制隆重出场了。

2 消息确认的两种机制

2.1 事务机制

注意,在发送一条消息之后,事务机制会阻塞发送端,直到 RabbitMQ 响应才会继续发送下一条消息,因此,事务机制的性能较差,会造成 RabbitMQ 的低吞吐量。

2.2 confirm 机制

与事务机制对比,confirm 机制更为轻量级。

我们需要注意的是,事务机制与 confirm 机制是互斥的,不可二者兼得。本博客将着重介绍 confirm 机制。

3 代码

下面我们采用 confirm 机制来实现消息确认,需要注意的是,我们采用了手动 ACK 来确认消息是否被正确接收(默认情况是使用自动 ACK)。若我们选择了手动 ACK 却又没有向 RabbitMQ 发送 ACK 的话,可能会导致一些严重的后果。

不多解释,直接先上代码

3.1 application.properties

server.port: 8080
spring.application.name: provider
spring.rabbitmq.host: 127.0.0.1
spring.rabbitmq.port: 5672
spring.rabbitmq.username: guest
spring.rabbitmq.password: guest
spring.rabbitmq.virtual-host: /

# 开启 confirm 确认机制
spring.rabbitmq.publisher-confirms: true
# 开启 return 确认机制
spring.rabbitmq.publisher-returns: true
# 手动应答
spring.rabbitmq.listener.simple.acknowledge-mode: manual
# 指定最小的消费者数量
spring.rabbitmq.listener.simple.concurrency: 1
# 指定最大的消费者数量
spring.rabbitmq.listener.simple.max-concurrency: 1
# 是否支持重试
spring.rabbitmq.listener.simple.retry.enabled: true

3.2 配置类

package com.example.provider.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 直连型交换机
 * @author 30309
 *
 */
@Configuration
public class DirectRabbitConfig {

	//队列,名称为DirectQueue
    @Bean
    public Queue DirectQueue() {
        return new Queue("DirectQueue",true);  //true表示是否持久 
    }
 
    //直连型交换机,名称为DirectExchange
    @Bean
    DirectExchange DirectExchange() {
        return new DirectExchange("DirectExchange");
    }
 
    //将队列和交换机绑定, 并设置用于匹配键:DirectRouting
    @Bean
    Binding bindingDirect() {
        return BindingBuilder.bind(DirectQueue()).to(DirectExchange()).with("DirectRouting");
    }
    
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { 
    	RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        // 消息发送失败返回到队列中, 配置文件需要配置 publisher-returns: true
        rabbitTemplate.setMandatory(true);
 
        // 消息返回, 配置文件需要配置 publisher-returns: true
        // ReturnCallback 接口用于实现消息发送到 RabbitMQ 交换器,但无相应队列与交换器绑定时的回调
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            System.out.println("消息发送失败:无相应队列与交换器绑定");
        });
 
        // 消息确认, 配置文件需要配置 publisher-confirms: true
        // ConfirmCallback 接口用于实现消息发送到 RabbitMQ 交换器后接收ack回调
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            if (ack) {
                System.out.println("消息发送成功:消息发送到 RabbitMQ 交换器");
            } else {
                System.out.println("消息发送失败:消息未发送到 RabbitMQ 交换器");
            }
        });
 
        return rabbitTemplate;
    }
}


3.3 生产者

package com.example.provider.controller;

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
 
/**
 * 生产者
 * @author 30309
 *
 */
@RestController
public class SendMessageController{
 
    @Autowired
    RabbitTemplate rabbitTemplate; 
 
    @GetMapping("/sendDirectMessage")
    public String sendDirectMessage() {
    	
        //将消息携带绑定键值DirectRouting发送到交换机DirectExchange
    	rabbitTemplate.convertAndSend("DirectExchange", "DirectRouting", "Hello World");
    	
        return "ok";
    }
 
}

3.4 消费者

package com.example.consumer.receiver;

import java.io.IOException;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import com.rabbitmq.client.Channel;

/**
 * 消费者1
 * @author 30309
 *
 */
@Component
public class DirectReceiver1 {
 
	@RabbitListener(queues = "DirectQueue")//监听的队列名称为DirectQueue
    @RabbitHandler
    public void process(String str,Channel channel, Message message) {
        System.out.println("DirectReceiver1消费者收到消息: " + str );

        
		try {
			channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
			//否认消息,被否认后会重新入队然后被再次消费
			//channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,true);
			//拒绝消息,消息会被丢弃,不会重回队列
			//channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

注意,我们使用 basicAck 需要传递两个参数,第一个参数指明唯一标识 ID,第二个参数为 true 时,可以一次性确认唯一标识 ID 小于等于传入值的所有消息(即批量处理手动确认,用于减少网络流量)。

唯一标识 ID:消费者向 RabbitMQ 注册后会建立一个 Channel ,当 RabbitMQ 向消费者推送消息时会携带了一个 ID,它代表 RabbitMQ 向该 Channel 投递的这条消息的唯一标识 ID,是一个单调递增的正整数。注意,唯一标识 ID 的范围仅限于 Channel。

3.5 测试结果

生产者:
在这里插入图片描述
消费者:
在这里插入图片描述
证明我们的测试成功

4 总结

  1. 一个消息被消费者正确接收后会从消息队列中移除,若一个队列没有被任何消费者订阅,那么这个队列中的消息会一直缓存下去
  2. 消息通过 ACK 确认是否被正确接收,每一条消息都需要被确认,其中确认又分为手动 ACK 和自动 ACK 两种,默认情况下使用自动 ACK 模式
  3. 自动 ACK 在消息发送给消费者后立即确认,但是也存在丢失消息的可能(消费端抛出异常) ,手动 ACK 则是指消费者调用 ack、nack、reject 几种方法进行确认,可以在业务失败后进行一些操作
  4. 若消息未被 ACK 则会发送给下一个消费者,如果某个服务忘记 ACK ,RabbitMQ 会认为该服务的处理能力有限,不会再发送数据给它

参考:springboot整合rabbitmq,支持消息确认机制
RabbitMQ:消息发送确认 与 消息接收确认(ACK)

发布了113 篇原创文章 · 获赞 206 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Geffin/article/details/102634259