rabbit死信队列出现TTL时间超过但是进入不了死信队列情况

原因是线上一场时间不精准问题导致的。

总的来说,为了让消息队列消息更加健壮,于是配置了超时时间和死信队列。但是出现的问题是,配置队列的TTL,总有一些消息在超过TTL时间后,进入不了死信队列,影响及时的业务通知系统。

问题在什么地方呢?

prefetch: 1

属性配置上。

以下是问题重现,与解决过程

1.环境搭建

1.1rabbit服务器略

1.2springboot工程略

1.3代码

消息队列配置类:

@Configuration
public class RabbitMQConfig {

    public static final String BUSINESS_EXCHANGE_NAME = "letter.demo.simple.business.exchange";
    public static final String BUSINESS_QUEUEA_NAME = "letter.demo.simple.business.queuea";
    public static final String BUSINESS_QUEUEB_NAME = "letter.demo.simple.business.queueb";
    public static final String DEAD_LETTER_EXCHANGE = "letter.demo.simple.deadletter.exchange";
    public static final String DEAD_LETTER_QUEUEA_ROUTING_KEY = "letter.demo.simple.deadletter.queuea.routingkey";
    public static final String DEAD_LETTER_QUEUEB_ROUTING_KEY = "letter.demo.simple.deadletter.queueb.routingkey";
    public static final String DEAD_LETTER_QUEUEA_NAME = "letter.demo.simple.deadletter.queuea";
    public static final String DEAD_LETTER_QUEUEB_NAME = "letter.demo.simple.deadletter.queueb";

    // 声明业务Exchange
    @Bean("businessExchange")
    public FanoutExchange businessExchange(){
        return new FanoutExchange(BUSINESS_EXCHANGE_NAME);
    }

    // 声明死信Exchange
    @Bean("deadLetterExchange")
    public DirectExchange deadLetterExchange(){
        return new DirectExchange(DEAD_LETTER_EXCHANGE);
    }

    // 声明业务队列A
    @Bean("businessQueueA")
    public Queue businessQueueA(){
        Map args = new HashMap<>(2);
//       x-dead-letter-exchange    这里声明当前队列绑定的死信交换机
        args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
//       x-dead-letter-routing-key  这里声明当前队列的死信路由key
        args.put("x-dead-letter-routing-key", DEAD_LETTER_QUEUEA_ROUTING_KEY);
//      设置队列最大存活时间
        args.put("x-message-ttl", 6000);
        return QueueBuilder.durable(BUSINESS_QUEUEA_NAME).withArguments(args).build();
    }

    // 声明业务队列B
    @Bean("businessQueueB")
    public Queue businessQueueB(){
        Map args = new HashMap<>(2);
//       x-dead-letter-exchange    这里声明当前队列绑定的死信交换机
        args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
//       x-dead-letter-routing-key  这里声明当前队列的死信路由key
        args.put("x-dead-letter-routing-key", DEAD_LETTER_QUEUEB_ROUTING_KEY);

        args.put("x-message-ttl", 6000);
        return QueueBuilder.durable(BUSINESS_QUEUEB_NAME).withArguments(args).build();
    }

    // 声明死信队列A
    @Bean("deadLetterQueueA")
    public Queue deadLetterQueueA(){
        return new Queue(DEAD_LETTER_QUEUEA_NAME);
    }

    // 声明死信队列B
    @Bean("deadLetterQueueB")
    public Queue deadLetterQueueB(){
        return new Queue(DEAD_LETTER_QUEUEB_NAME);
    }

    // 声明业务队列A绑定关系
    @Bean
    public Binding businessBindingA(@Qualifier("businessQueueA") Queue queue,
                                    @Qualifier("businessExchange") FanoutExchange exchange){
        return BindingBuilder.bind(queue).to(exchange);
    }

    // 声明业务队列B绑定关系
    @Bean
    public Binding businessBindingB(@Qualifier("businessQueueB") Queue queue,
                                    @Qualifier("businessExchange") FanoutExchange exchange){
        return BindingBuilder.bind(queue).to(exchange);
    }

    // 声明死信队列A绑定关系
    @Bean
    public Binding deadLetterBindingA(@Qualifier("deadLetterQueueA") Queue queue,
                                    @Qualifier("deadLetterExchange") DirectExchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with(DEAD_LETTER_QUEUEA_ROUTING_KEY);
    }

    // 声明死信队列B绑定关系
    @Bean
    public Binding deadLetterBindingB(@Qualifier("deadLetterQueueB") Queue queue,
                                      @Qualifier("deadLetterExchange") DirectExchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with(DEAD_LETTER_QUEUEB_ROUTING_KEY);
    }
}

业务监听类:

@Slf4j
@Component
public class BusinessMessageReceiver {

    @RabbitListener(queues = BUSINESS_QUEUEA_NAME)
    public void receiveA(Message message, Channel channel) throws IOException {


        /**
         * 模拟休息
         */
        try {
            Thread.sleep(1000*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        String msg = new String(message.getBody());
        log.info("收到业务消息A:{}", msg);
        boolean ack = true;
        Exception exception = null;
        try {
            if (msg.contains("deadletter")){
                throw new RuntimeException("dead letter exception");
            }
        } catch (Exception e){
            ack = false;
            exception = e;
        }
        if (!ack){
            log.error("消息消费发生异常,error msg:{}", exception.getMessage(), exception);
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
        } else {
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        }
    }

    @RabbitListener(queues = BUSINESS_QUEUEB_NAME)
    public void receiveB(Message message, Channel channel) throws IOException {

        /**
         * 模拟休息
         */
        try {
            Thread.sleep(1000*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("收到业务消息B:" + new String(message.getBody()));
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
}

死信队列监听类:

@Component
public class DeadLetterMessageReceiver {


    @RabbitListener(queues = DEAD_LETTER_QUEUEA_NAME)
    public void receiveA(Message message, Channel channel) throws IOException {
        System.out.println("收到死信消息A:" + new String(message.getBody()));
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }

    @RabbitListener(queues = DEAD_LETTER_QUEUEB_NAME)
    public void receiveB(Message message, Channel channel) throws IOException {
        System.out.println("收到死信消息B:" + new String(message.getBody()));
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
}

消息发送类:

@Component
public class BusinessMessageSender {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendMsg(String msg){
        rabbitTemplate.convertSendAndReceive(BUSINESS_EXCHANGE_NAME, "", msg);
    }
}

消息发送接口:

@RequestMapping("rabbitmq")
@RestController
public class RabbitMQMsgController {

    @Autowired
    private BusinessMessageSender sender;

    @RequestMapping("sendmsg")
    public void sendMsg(String msg){
        sender.sendMsg(msg);
    }
}

application.yml:

server:
  port: 8088
spring:
  rabbitmq:
    host: 192.168.83.11
    username: nc
    password: nc
    virtual-host: /nc
    listener:
      type: simple
      simple:
        default-requeue-rejected: false
        acknowledge-mode: manual


  application:
    name: miaosha-service
  datasource:
    url: jdbc:mysql://localhost:3306/ttt?useUnicode=true&characterEncoding=UTF-8
    username: root
    password: root
    hikari:
      max-lifetime: 28830000 # 一个连接的生命时长(毫秒),超时而且没被使用则被释放(retired),缺省:30分钟,建议设置比数据库超时时长少30秒,参考MySQL wait_timeout参数(show variables like '%timeout%';)
      maximum-pool-size: 9 # 连接池中允许的最大连接数。缺省值:10;推荐的公式:((core_count * 2) + effective_spindle_count)
  redis:
    host: 192.168.83.11

pom:

    
        nc-item
        com.nc.item
        1.0.0-SNAPSHOT
    
    4.0.0

    nc-test-sha


    
        
            org.springframework.boot
            spring-boot-starter-data-redis
        
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
        
        
        
            org.mybatis.spring.boot
            mybatis-spring-boot-starter
        
        
        
            tk.mybatis
            mapper-spring-boot-starter
        
        
        
            com.github.pagehelper
            pagehelper-spring-boot-starter
        
        
        
            org.springframework.boot
            spring-boot-starter-jdbc
        
        
        
            mysql
            mysql-connector-java
        
        
            com.nc.item
            nc-item-interface
            1.0.0-SNAPSHOT
        
        
        
            org.springframework.boot
            spring-boot-starter-actuator
        

        
            com.nc.common
            nc-common
            1.0.0-SNAPSHOT
        

        
            org.springframework.boot
            spring-boot-starter-amqp
        

        
            org.springframework.boot
            spring-boot-starter-test
        

        
            com.alibaba
            fastjson
            1.2.54
        

        
            org.projectlombok
            lombok
            provided
            1.16.22
        
    

说明,其实这里配置了TTL过期时间是6s。客户端连接后,进行线程暂停,时间超过TTL时间。于是会发现,应该转移到死信队列的消息,一直停留在原队列中。

rabbit死信队列出现TTL时间超过但是进入不了死信队列情况_第1张图片

点击查看消费者消息:

rabbit死信队列出现TTL时间超过但是进入不了死信队列情况_第2张图片 会发现 我队列参数设置是什么没问的,但是就是成为不了死信。

也就是说,你认为消息应该死了,但是实际上它还驻留在原队列中没有被消费。

2原因分析

当监听者进行消费时,如果出现了系统资源紧张,程序出错,长期hang住,那么死信队列的消息就不能及时让死信队列监听者消费,影响后续业务逻辑。

可问题是,为什么这些TTL的消息不进入配置的死信队列呢?

2.1消息队列消费机制

这个你自己复习一下好了,我不再赘述

2.2消息队列优化参数中,有一个prefetch参数

rabbitmq默认的参数是250,也就是说监听者会一次性抓取250条消息进行批量消费,这样效率更高

2.3和咱们有什么关系

我们实验场景消息队列基本没有积压,就是1,2条消息。客户端再消费第一条消息的时候,进行了休眠,导致后面的消息在同一个package中,说消费完了,也没有,一直就是在消费中的状态。。。但是也一直没有ack掉。所以,你的消息一直进入不了死信队列。

3解决

适当的调整prefetch参数

本例中,将prefetch=1设置上,基本不会出现问题了

那是不是,业务中要设置成为1呢?很显然,不是的。因为你要兼顾效率呀。

rabbit死信队列出现TTL时间超过但是进入不了死信队列情况_第3张图片

4感悟 

千万不要人云亦云,一定要上手实践,多看官网。

你可能感兴趣的:(java)