RabbitMQ 如何保证消息一定会发送成功?【RabbitMQ Publisher Confirm 机制】

前言:

我们知道 RabbitMQ 保证消息一定会发送成功,一般有两种方式,分别是 Publisher Confirm 机制和事务消息,本篇分享如何使用 Publisher Confirm 来保证消息一定发送成功。

RabbitMQ 系列文章传送门

RabbitMQ 的介绍及核心概念讲解

@RabbitListener 注解详解

Spring Boot 整合 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 保证消息一定会成功发送的代码,希望可以帮助到有需要的小伙伴。

如有不正确的地方欢迎各位指出纠正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值