前言:
我们知道 RabbitMQ 保证消息一定会发送成功,一般有两种方式,分别是 Publisher Confirm 机制和事务消息,本篇分享如何使用 Publisher Confirm 来保证消息一定发送成功。
RabbitMQ 系列文章传送门
RabbitMQ Publisher Confirm 机制的原理
RabbitMQ Publisher Confirm 是一种消息确认机制,生产者将信道 Channel 设置为 Confirm 模式,在该信道上发送的消息都会被指派一个唯一的 ID,如果消息成功的发送到交换机 Exchange 和队列 Queue 上之后,RabbitMQ 就会给生产者发送一个确认信息给到生产者(使用的是函数回调机制),确认信息中包含消息的唯一 ID,这样生产者就知道消息是否成功发送了,也就是 Publisher Confirm 机制(Confirm 机制是异步的)。
RabbitMQ Publisher Confirm 机制的策略
RabbitMQ Publisher Confirm 机制有两种策略,分别是对应交换机和队列。
- Publisher Confirm:生产者发送的消息到达交换机 Exchange 时候会触发。
- Publisher return:生产者发送的消息到达队列 Queue 时候会触发。
RabbitMQ Publisher Confirm 机制开启方式
在配置文件中添加如下配置
spring.rabbitmq.publisher-confirm-type=correlated
该配置有三个值,具体如下:
- none:禁用发布确认模式,是默认值。
- correlated:是发布消息成功到交换器后会触发回调方法。
- simple:有两种效果,第一种和 correlated 一样会触发回调方法,第二种在消息发送成功后使用 rabbitTemplate 调用 waitForConfirms 或 waitForConfirmsOrDie 方法等待 Broker 返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是 waitForConfirmsOrDie 方法如果返回 false 则会关闭 Channel,则接下来无法发送消息到 Broker。
RabbitMQ Publisher Confirm 保证消息发送成功
Spring Boot 集成 RabbitMQ
在 proerties 或者 yml 文件中添加 RabbitMQ 配置如下:
spring.rabbitmq.host= xxx.xxx.xxx
spring.rabbitmq.port= 5672
spring.rabbitmq.username= admin
spring.rabbitmq.password= admin
spring.rabbitmq.virtual-host = /study
项目 pom.xml 文件中引入 spring-boot-starter-amqp 依赖如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>2.4.5</version>
</dependency>
配置交换机 Exchange、路由 RoutingKey、队列 Queue
代码如下:
@Configuration
public class RabbitConfirmConfig {
//注入队列
@Bean("confirmQueue")
public Queue queue() {
return new Queue("direct-confirm-queue");
}
//注入交换机
@Bean("directConconfirmExchange")
public DirectExchange confrimDirectExchange() {
//durable:重启后是否有效 autodelete: 长期未使用是否删除掉
return new DirectExchange("direct-confirm-exchange", true, true);
}
//绑定队列和交换机
@Bean("directConfirmBinding")
public Binding binding() {
return BindingBuilder.bind(queue()).to(confrimDirectExchange()).with("direct-confirm-exchange-routing-key");
}
//配置 RabbitTemplate
@Bean("rabbitTemplate")
public RabbitTemplate transactionRabbitTemplate(ConnectionFactory connectionFactory) {
return new RabbitTemplate(connectionFactory);
}
}
使用 RabbitTemplate 实现 RabbitMQ Publisher Confirm
消息生产者代码如下:
@Slf4j
@Component
public class RabbitConfirmProducer implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
//@PostConstruct注解的作用是在对象创建后执行一些需要在对象注入完毕后进行的操作
@PostConstruct
public void init() {
//设置交换机处理失败消息的模式 true 表示消息由交换机到达不了队列时 会将消息重新返回给生产者
//如果不设置这个指令 则交换机向队列推送消息失败后 不会触发 setReturnCallback
rabbitTemplate.setMandatory(true);
//手动 ACK
rabbitTemplate.setConfirmCallback(this);
}
public void sendConfrimMessage(String message) {
//发送消息
boolean result = sendMessage("direct-confirm-exchange", "direct-confirm-exchange-routing-key", message);
int count = 0;
if (!result) {
while (count < 3 && !result) {
result = sendMessage("direct-confirm-exchange", "direct-confirm-exchange-routing-key", message);
count++;
}
}
log.info("重试次数:{}", count);
}
//waitForConfirms 方法只有消息正常发送才会 true,否则就会抛出异常,我们捕获了异常,就可以根据自定义结果来判断消息的发送情况,正对做出处理
public boolean sendMessage(String exchange, String routingKey, String message) {
//消息id 自己看情况处理
String msgId = UuidUtils.generateUuid();
CorrelationData correlationData = new CorrelationData(msgId);
try {
return Boolean.TRUE.equals(rabbitTemplate.invoke(operations -> {
rabbitTemplate.convertAndSend(
exchange,
routingKey,
message,
correlationData
);
//等待两秒
return rabbitTemplate.waitForConfirms(2 * 1000);
}));
} catch (Exception e) {
log.error("Publihser Confirm Error...", e);
return false;
}
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
log.info("confirm correlationData:{},ack:{},cause:{}", JSON.toJSONString(correlationData), ack, cause);
if (ack) {
//交换机收到
log.info("confirm 交换机收到消息 ack:{}", ack);
} else {
//交换机没有收到
log.error("confirm 交换机没有收到消息 ack:{}", ack);
}
}
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
log.info("ReturnedMessage:{}", JSON.toJSONString(returnedMessage));
}
}
测试验证:
我们使用 direct-confirm-exchange1 作为交换机来发送消息,得到如下结果:
符合预期。
我们再此使用前面我们声明好的 direct-confirm-exchange 作为交换机来发送消息,得到如下结果:
使用已经声明过的交换机发送消息,正常完成发送,符合预期。
如果我们不想同步等待发送结果,使用如下方式发送消息即可。
rabbitTemplate.convertAndSend(exchange,routingKey,message);
使用 Channel 实现 RabbitMQ Publisher Confirm
封装 RabbitMqUtil 来获取 Channel,代码如下:
@Slf4j
public class RabbitMqUtil {
/**
* @return com.rabbitmq.client.Channel
* @date 2024/9/6 10:43
* @description 获取 RabbitMQ Channel
*/
public static Channel getChannel() {
// 创建一个连接工厂,并设置MQ的相关信息
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("xxxxxx");
factory.setUsername("xxx");
factory.setPassword("xxx");
factory.setVirtualHost("/xxx");
Channel channel = null;
try {
// 创建连接
Connection connection = factory.newConnection();
// 获取信道
channel = connection.createChannel();
} catch (Exception e) {
log.error("创建 RabbitMQ Channel 失败", e);
e.printStackTrace();
}
return channel;
}
}
使用 Channel 实现的消息生产者代码如下:
@Slf4j
@Component
public class RabbitChannelConfirmProducer {
public void sendChannelConfirmMessage(String message) {
//发送消息
boolean result = sendMessage("direct-confirm-exchange", "direct-confirm-exchange-routing-key", message);
if (!result) {
//发送失败 重试
int count = 0;
while (count < 3 && !result) {
result = sendMessage("direct-confirm-exchange", "direct-confirm-exchange-routing-key", message);
count++;
}
log.info("重试次数:{}", count);
}
log.info("消息发送结果:{}", result);
}
public boolean sendMessage(String exchange, String routingKey, String message) {
//获取 RabbitMQ Channel
Channel channel = RabbitMqUtil.getChannel();
try {
//启用 confirm 模式
channel.confirmSelect();
//启用 confrim 回调
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
//确认消息
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
//未确认消息
}
});
//启用 return 回调
channel.addReturnListener(new ReturnListener() {
@Override
public void handleReturn(int i, String s, String s1, String s2, AMQP.BasicProperties basicProperties, byte[] bytes) throws IOException {
//处理发送到 queue 失败的消息
log.error("消息发送到 queue 失败");
}
});
//发布消息
channel.basicPublish(exchange, routingKey, true, null, message.getBytes(StandardCharsets.UTF_8));
//等待 confirm 结果 waitForConfirms 方法只有消息发送成功的时候会返回 true 发送失败会直接抛出异常
if (channel.waitForConfirms()) {
log.info("waitForConfirms 消息发送到交换机 Exchange 成功");
return true;
}
} catch (Exception e) {
log.error("waitForConfirms 消息发送失败", e);
return false;
} finally {
try {
//关闭 channel
channel.close();
} catch (Exception e) {
log.error("channel close error", e);
e.printStackTrace();
}
}
return false;
}
}
消费者消息 ACK
消费者消费使用手动 ACK 代码如下:
@Slf4j
@Component
public class RabbitConfirmMessageConsumer {
//direct confirm 模式消费端
@RabbitListener(queues = "direct-confirm-queue")
public void directConfirmMessageConsumer(String messageData, Channel channel, Message message) {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
log.info("confirm 消息消费开始,消息内容:{},deliveryTag:{}", messageData, deliveryTag);
//模拟业务
channel.basicAck(deliveryTag, true);
} catch (IOException e) {
//NACK multiple:是否批量处理 true:将一次性ack所有小于 deliveryTag 的消息 false 就不 ACK requeue:为true时, 重新入队
try {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
} catch (IOException ex) {
ex.printStackTrace();
}
e.printStackTrace();
}
}
}
触发消息发送代码如下:
@Autowired
private RabbitChannelConfirmProducer rabbitChannelConfirmProducer;
@GetMapping("/send-channel-confirm-message")
private String sendChannelConfirmMessage(@RequestParam String message) {
rabbitChannelConfirmProducer.sendChannelConfirmMessage(message);
return "OK";
}
总结:本篇分享了两种使用 RabbitMQ 保证消息一定会成功发送的代码,希望可以帮助到有需要的小伙伴。
如有不正确的地方欢迎各位指出纠正。