SpringBoot+RabbitMQ_消息确认机制、消息重试机制

一、前言

    使用RabbitMQ,会有知晓消息是否成功、如果消费失败重试的需求,这篇文章主要讲的是消息确认机制(ACK)和消息重试机制。

二、消息确认机制

    RabbitMQ的消费者确认机制用来确认消费者是否成功消费了队列中的消息。

    消息确认分为几种情况:

    AcknowledgeMode.NONE:不确认,不发送任何ack确认;只要消息发送完成会立即在队列移除,不会重发。
    AcknowledgeMode.AUTO:自动确认,消费消息后会自动发送ack确认;在消费者发生异常时,消息会不断的被重发,直到处理成功。不会丢失消息,即便服务挂掉,没有处理完成的消息会重回队列,但是异常会让消息不断重试。
    AcknowledgeMode.MANUAL:手动确认,手动发送ack确认;只有服务端收channel.basicAck(message.getMessageProperties().getDeliveryTag(),false)的确认信号,消息才会移除,确认成功后不管后面是异常还是断开服务消息已经被移除了。如果在确认之前抛出异常,消息不会移除,也不会重试,监听程序会因为异常停止不再处理消息 ,如果此时断开服务,消息重新回到队列。

具体实现:

1、开启消息手动确认,修改application配置文件

spring.rabbitmq.listener.simple.acknowledge-mode=manual

2、消费者

    @RabbitListener(queues = "meat_queue")
    @RabbitHandler
    public void processMeatOne(String content, Channel channel, Message message) throws IOException{
        try {
            System.out.println("processMeatOne---开始消费队列meat_queue的消息: " + content);
            // 模拟执行任务
            Thread.sleep(1000);
            // 模拟异常
            String is = null;
            is.toString();
            // 确认收到消息,false只确认当前consumer一个消息收到,true确认所有consumer获得的消息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception  e) {
            // 为了避免MQ中Unacked的消息堆积,消费失败也进行ack确认
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            e.printStackTrace();
        }
    }

    但是异常有可能是网络等问题,还想再重试一次,再决定是否确认或者抛弃这个异常,我们在异常捕获里进行处理,

    catch (Exception  e) {
            System.out.println("=====================异常了========================");
            if (message.getMessageProperties().getRedelivered()) {
                System.out.println("================消息已重复处理失败,拒绝再次接收======================" + content);
                // 拒绝消息,requeue=false 表示不再重新入队,如果配置了死信队列则进入死信队列
                channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
            } else {
                System.out.println("====================消息即将再次返回队列处理=========================" + content);
                // requeue为是否重新回到队列,true重新入队
                channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
            }
            //e.printStackTrace();
        }

    如果单纯的做channel.basicNack处理,可能造成死循环。

    在手动确认的模式下,不管是消费成功还是消费失败,一定要记得确认消息,不然消息会一直处于unack状态,直到消费者进程重启或者停止。

3、方法详解:   

  确认收到一个或多个消息:

void basicAck(long deliveryTag, boolean multiple) throws IOException;

    deliveryTag:消息的传递标识。

    multiple: 如果为false,只确认当前consumer一个消息;如果为true,则确认所有consumer获得的所有小于deliveryTag的消息。

  拒绝一个或多个消息:

void basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException;

    deliveryTag:消息的传递标识。

    multiple: 如果为true,则拒绝所有consumer获得的小于deliveryTag的消息。

    requeue: 设置为true 会把消费失败的消息从新添加到队列的尾端,设置为false不会重新回到队列。

  拒绝一个消息:

void basicReject(long deliveryTag, boolean requeue) throws IOException;

    deliveryTag:消息的传递标识。

    requeue: 设置为false 表示不再重新入队,如果配置了死信队列则进入死信队列。
 

    channel.basicNack 与 channel.basicReject 的区别在于basicNack可以批量拒绝多条消息,而basicReject一次只能拒绝一条消息。

三、消息重试

    重试并不是RabbitMQ重新发送了消息,仅仅是消费者内部进行的重试,换句话说就是重试跟mq没有任何关系;

    自动ack模式下,如果超出了重试次数,队列中的数据会被ack掉;

    自动ack模式下,使用catch捕获异常也是会导致不触发重试的;因此消费者代码不能添加try{}catch(){}捕获异常,一旦捕获了异常,就相当于消息正确处理了,消息直接被确认掉了,不会触发重试;

具体实现:

1、开启消息自动确认,开启消息重试,修改application配置文件

# 开启重试,默认是false
spring.rabbitmq.listener.simple.retry.enabled=true
# 重试次数,默认为3次
spring.rabbitmq.listener.simple.retry.max-attempts=5
# 重试最大间隔时间
spring.rabbitmq.listener.simple.retry.max-interval=10000
# 重试初始间隔时间
spring.rabbitmq.listener.simple.retry.initial-interval=2000
# 间隔时间乘子,间隔时间*乘子=下一次的间隔时间,最大不能超过设置的最大间隔时间
spring.rabbitmq.listener.simple.retry.multiplier=2
# 消费自动确认
spring.rabbitmq.listener.simple.acknowledge-mode=auto

2、消费者

    @RabbitListener(queues = "meat_queue")
    @RabbitHandler
    public void processMeatTwo(String message) throws InterruptedException {
        System.out.println("processMeatTwo消费了队列meat_queue的消息:" + message);
        Thread.sleep(1000);
        //模拟异常
        String is = null;
        is.toString();
    }

3、运行结果

    控制台中可以看到,消息一共重试了5次,之后会抛出ListenerExecutionFailedException的异常,后面附带着Retry Policy Exhausted文字,提示我们重试次数已经用尽了。

    消息重试次数用尽后,消息就会被抛弃,但是我们不想消息被抛弃,可以采用死信队列的方式处理重试失败的消息。

4、死信队列

  声明死信交换机dlx_exchange,死信队列dlx_queue,业务队列添加死信交换机以及死信路由键的配置。队列,交换机,以及绑定关系的声明如下:

@Configuration
public class RabbitConfig {

    /**
     * 声明2个业务队列
     **/
    @Bean
    public Queue fruitsQueue(){
        return new Queue("fruits_queue");
    }

    @Bean
    public Queue meatQueue(){
        Map<String,Object> params = new HashMap<>();
        params.put("x-dead-letter-exchange","dlx_exchange");//声明当前队列绑定的死信交换机
        params.put("x-dead-letter-routing-key","dlx");//声明当前队列的死信路由键
        return QueueBuilder.durable("meat_queue").withArguments(params).build();
    }

    /**
     * 死信队列
     * @return
     */
    @Bean
    public Queue dlxQueue(){
        return new Queue("dlx_queue");
    }

    /**
     * 声明一个Direct类型的交换机
     **/
    @Bean
    DirectExchange directExchange(){
        return new DirectExchange("xue_exchange");
    }
    
    /**
     * 死信交换机
     * @return
     */
    @Bean
    public DirectExchange dlxExchange(){
        return new DirectExchange("dlx_exchange");
    }

    /**
     * 将上面的2个队列绑定到Direct交换机,在绑定的时候指定BindingKey
     **/
    @Bean
    Binding bindExchangeFruits(Queue fruitsQueue, DirectExchange directExchange){
        return BindingBuilder.bind(fruitsQueue).to(directExchange).with("fruits");
    }

    @Bean
    Binding bindExchangeMeat(Queue meatQueue,DirectExchange directExchange){
        return BindingBuilder.bind(meatQueue).to(directExchange).with("meat");
    }

    /**
     * 死信队列绑定死信交换机
     * @param dlxQueue 队列
     * @param dlxExchange 交换机
     * @return
     */
    @Bean
    Binding dlcBinding(Queue dlxQueue, DirectExchange dlxExchange){
        return BindingBuilder.bind(dlxQueue).to(dlxExchange).with("dlx");
    }
}

5、测试一下

正在消费时:

重试次数使用完之后,会看到dlx_queue队列中有一条待消费的消息:

也就是说重试次数使用完之后,消息会从业务队列中删除,同时发送到死信队列中。

会发现meat_queue队列比fruits_queue队列多出了DLX和DLK两个标识。

DLX ,指的是x-dead-letter-exchange 这个属性,全称为 Dead-Letter-Exchange ,可以称之为死信交换机。当消息在一个队列中变成死信 (dead message) 之后,它能被重新被发送到另一个交换机中,这个交换机就是 DLX,绑定 DLX 的队列就称之为死信队列。DLX 也是一个正常的交换机,和一般的交换机没有区别,它能在任何的队列上被指定 , 实际上就是设置某个队列的属性。当这个队列中存在死信时 , RabbitMQ 就会自动地将这个消息重新发布到设置的 DLX 上去 ,进而被路由到另一个队列,即死信队列,可以监听这个队列中消息做相应的处理。

DLK,指的是x-dead-letter-routing-key 这个属性,关联的是routingKey,也就是路由。

~OVER~

  • 4
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值