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