[RabbitMQ] 消息确认模式在SpringBoot中的使用

RabbitMQ有两种消息确认模式:

    ① 消息发送确认

    ② 消息消费确认

首先说一下消息发送确认模式:

消息确认模式也可以细分为两种:

  1. 第一种publisher-confirms模式,该模式只关心消息有没有被传递到指定Exchang,而不关心消息有没有被路由到queues
  2. 第二种publisher-returns模式,该模式会在消息没有被路由到queues时将消息返回

下面的代码一起实现了两种模式:

① 在SpringBoot的配置文件中添加如下配置:

spring.rabbitmq.publisher-confirms=true
spring.rabbitmq.template.mandatory=true

② 实现回调接口

如上文所说,消息发送确认模式下有两种不同的子模式,如果想都启用则需要实现两个不同的回调接口:

@Component
public class CallBackSender implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {

     @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            System.out.println("消息[" + correlationData.getId() + "]成功发送到指定ExChange");
        } else {
            System.out.println("消息[" + correlationData.getId() + "]发送到ExChange失败:" + cause);
        }
    }

    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        System.out.println("消息未能成功路由到指定queues");
        System.out.println("return--message:" + new String(message.getBody()) + ",replyCode:" + replyCode
                + ",replyText:" + replyText + ",exchange:" + exchange + ",routingKey:" + routingKey);
    }
}

③ 启用配置并传入回调接口实现

在你的RabbitMQConfiguration配置类里,启用配置,同时将刚刚实现的两个回调接口传入,参考代码如下:

@Configuration
public class RabbitMQConfiguration {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

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

    @Value("${spring.rabbitmq.port}")
    private int port;

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

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

    @Value("${spring.rabbitmq.publisher-confirms}")
    private boolean publisherConfirm;

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

    @Value("${spring.rabbitmq.template.mandatory}")
    private boolean mandatory;

    @Bean
    public ConnectionFactory connectionFactory() {
        CachingConnectionFactory connectionFactory = new CachingConnectionFactory(host,port);
        connectionFactory.setUsername(username);
        connectionFactory.setPassword(password);
        connectionFactory.setVirtualHost(virtualHost);
        connectionFactory.setPublisherConfirms(publisherConfirm); // 开启消息确认模式
        return connectionFactory;
    }
    
    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    /* 此处设置为prototype是因为一个RabbitTemplate实例只能被设置一次ConfirmCallBack,否则会报错
       所以如果想保持singleton类型,则需要保证template不会被重复setConfirmCallBack */
    public RabbitTemplate rabbitTemplate() {
        RabbitTemplate template = new RabbitTemplate(connectionFactory());
        template.setMandatory(mandatory); // 开启消息返回模式
        // 将两种不同确认模式的回调接口实例传入
        template.setConfirmCallback(new CallBackSender()); 
        template.setReturnCallback(new CallBackSender());
        return template;
    }

}

这样就算配置完成了,接下来进行测试:

发送的方法具体代码如下(省去controller等):

public void send() {
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        System.out.println("生成消息成功,id:" + correlationData.getId());

        try {
            this.rabbitTemplate.convertAndSend("exchange1", "test-ack1",
                    "测试".getBytes("UTF-8"), correlationData);
            System.out.println("消息发送到RabbitMQ服务器成功,id:" + correlationData.getId());
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }

 

由于我的RabbitMQ服务器中存在名为exchange1的Exchange,但该Exchange没有名为test-ack1的binding。所以此处消息能被发送Exchange,但是无法被路由到指定的queue。而输出的测试信息与情况吻合:

可以看到,消息确实是成功发送到了Exchange,因此ConfirmCallBack在调用confirm方法时传入了ack=true,但是由于指定的routingKey=test-ack1并不存在,所以消息未能成功路由到queue,因此ReturnCallback的returnedMessage方法也被调用。

 

消息消费确认模式

在RabbitMQ中,消息被Consumer从MQ中获取后,如果没有特殊设置,这条消息会自动被确认消费然后从MQ中删除。

在这种自动确认模式下,如果Consumer在处理某条消息时出现了异常,无法成功处理该条消息,而又由于自动确认模式下,这条消息已经被RabbitMQ从队列删除,所以这条信息最后将无法复原。

想改变这种情况可以设置RabbitMQ为手动确认模式,这样在消息被获取后,如果处理成功,则可以通过

channel.basicAck(long deliveryTag, boolean multiple);

方法来告诉RabbitMQ该条消息已被确认。

如果处理失败,则可以通过

channel.basicNack(long deliveryTag, boolean multiple, boolean requeue);

方法来告诉RabbitMQ处理失败,其中requeue变量是用来告诉RabbitMQ是否将这条信息重新返回到队列中,此处需要注意避免死锁,具体的可以参考这篇文章:https://www.jianshu.com/p/7d1a0f4a9da5?utm_source=oschina-app

实现demo如下:

@Bean
    public SimpleMessageListenerContainer messageContainer() {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory());
        container.setQueueNames("test-ack");
        container.setExposeListenerChannel(true);
        container.setMaxConcurrentConsumers(1);
        container.setConcurrentConsumers(1);
        container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        container.setMessageListener(new ChannelAwareMessageListener() {

            public void onMessage(Message message, Channel channel) throws Exception {
                try {
                    long deliverTag = message.getMessageProperties().getDeliveryTag();
                    System.out.println(
                            "消费端接收到消息:" + new String(message.getBody()));
                    System.out.println("[deliverTag:" + deliverTag + "] " + message.getMessageProperties().getReceivedRoutingKey());

                    if (deliverTag%2 == 0) {
                        System.out.println("消息处理失败,重新返回队列");
                        throw new Exception("数据处理错误");
                    }

                    channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
                } catch (Exception e) {
                    e.printStackTrace();
                    channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
                }
            }
        });
        return container;

    }

在相关配置类中声明SimpleMessageListenerContainer,这是一个默认的实现类,在其中通过

container.setAcknowledgeMode(AcknowledgeMode.MANUAL) 

来开启手动确认模式,并且在最后的MessageListener中手动返回ack或者nack。

你可能感兴趣的:(RabbitMQ)