RabbitMq Spring Boot 消费侧批处理能力重试机制

158 阅读6分钟

解决问题

1.队列消息积压

2.消息丢失,IO波动导致消费失败缺乏重试机制

RabbitMq队列配置

/**  
* RabbitMq配置  
* @author ChenYu ren  
* @date 2024/9/3  
*/  
  
@Configuration  
public class TestRabbitMqConfig {
  
    /**  
    * 交换机名称  
    */  
    public static final String EXCHANGE_NAME = "TestExchange";  

    /**  
    * pushMessage to exchange route queue 【RoutingKey】  
    */  
    public static final String ROUTING_KEY = "TestRk";  

    /**  
    * 队列名称  
    */  
    public static final String QUEUE = "TestQueue";  
  
  
  
    @Bean("testQueue")  
    public Queue queue() {  
        //第二个参数表示是否持久化
        return new Queue(QUEUE, true);  
    }
  
    @Bean("testExchange")  
    public DirectExchange directExchange() {  
        return new DirectExchange(EXCHANGE_NAME);  
    }
  
  
    /**  
    * 交换机与队列绑定  
    * @return 绑定关系  
    */  
    @Bean("queueBindExchange")  
    public Binding healthyBindStaExchange(@Qualifier("testExchange")DirectExchange exchange,  
    @Qualifier("testQueue")Queue queue) {  
        return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY);  
    }
  
    @Bean("testRabbitListenerContainerFactory")  
    public RabbitListenerContainerFactory<SimpleMessageListenerContainer> rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {  
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();  
        factory.setConnectionFactory(connectionFactory);  
        // 启用批量消费  
        factory.setBatchListener(true);  
        //用于在容器中创建消息批处理  
        factory.setConsumerBatchEnabled(true);  
        // 批量大小  
        factory.setBatchSize(3);  
        // 手动确认  
        factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);  
        //接收等待时长超时时间设置  
        //1.我们设置了BatchSize=3,如果等待了30s还消息数量<3,则会直接给消费者推送  
        factory.setReceiveTimeout(30 * 1000L);  
        return factory;  
    }  
  
}

消费者(Consumer)

/**
 * RabbitMq消费者
 * @author ChenYu ren
 * @date 2024/9/3
 */

@Slf4j
@Component
public class TestRabbitMqConsumer {

    @Resource
    private AmqpTemplate rabbitTemplate;

    @RabbitListener(queues = TestRabbitMqConfig.QUEUE,containerFactory = "testRabbitListenerContainerFactory")
    public void consumerMessage(Channel channel, List<Message> messageList) {
        messageList.forEach(message -> {
            String messageJson = new String(message.getBody(), StandardCharsets.UTF_8);
            try {
                log.info("consumer begin message -> {}",messageJson);
                //业务处理
                handleMessage(messageJson);
                // 消息处理完成ack
                channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            } catch (Exception e) {
                try {
                    log.error("消息消费失败,开始重试或入库人工介入 message -> {}, errorMsg -> {}",messageJson,e.getMessage());
                    Integer retryCount = (Integer) message.getMessageProperties().getHeaders().getOrDefault("x-retry-count", 0);
                    retryCount = retryCount + 1;
                    message.getMessageProperties().getHeaders().put("x-retry-count", retryCount);
                    if (retryCount >= 3) {
                        //将消息发送的死信队列 | 入库报警人工介入
                        sendDeadQueueOrSaveLog(messageJson);
                        channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);

                    }else {
                        log.error("消费失败,发布消息重试准备第{}次重试 message ->{}",retryCount,messageJson);
                        //重试次数重置,拒绝消息等待重新消费
                        channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);

                    }
                }catch (Exception exception){
                    log.error("消息消费失败,重新发送消息失败,或入库失败 message -> {} errorMsg -> {}",messageJson,exception.getMessage());
                }
            }
        });

    }


    /**
     * 处理消息
     * @param message 消息
     */
    private void handleMessage(String message){
        JSONObject jsonObject = JSONObject.parseObject(message);
        if (jsonObject.getInteger("userId") == 1){
            throw new CustomException("模拟业务异常");
        }
        log.info("handleMessage consumer success message -> {} ",message);
    }

    /**
     * 消息消费重试次数达到最大上限,发送到死信队列或入库人工介入
     * @param message 消息
     */
    private void sendDeadQueueOrSaveLog(String message){
        log.info("消息消费失败,重试次数达到上限。写入死信队列或入库 成功 message -> {} ",message);
    }

}

存在问题: 死循环

原因: 消息的 x-retry-count 标记没有被正确更新,这导致每次从队列中取出消息时,重试计数(retryCount)总是从 0 开始。这是因为 RabbitMQ 并不会自动将消息的属性(例如自定义头 x-retry-count)持久化到消息队列中。

原因分析: RabbitMQ 的消息头信息(包括你添加的 x-retry-count)在消息被重新入队(requeue = true)时,不会被修改或更新。这意味着每次你重新入队消息时,x-retry-count 的值不会被保留。

输出:
2024-09-14 16:24:18.484  INFO 53942 --- [tContainer#12-1] c.q.p.s.h.consumer.TestRabbitMqConsumer  : consumer begin message -> {"userId":1}
2024-09-14 16:24:18.491 ERROR 53942 --- [tContainer#12-1] c.q.p.s.h.consumer.TestRabbitMqConsumer  : 消息消费失败,开始重试或入库人工介入 message -> {"userId":1}, errorMsg -> 模拟业务异常
2024-09-14 16:24:18.492 ERROR 53942 --- [tContainer#12-1] c.q.p.s.h.consumer.TestRabbitMqConsumer  : 消费失败,发布消息重试准备第1次重试 message ->{"userId":1}
2024-09-14 16:24:21.518  INFO 53942 --- [tContainer#12-1] c.q.p.s.h.consumer.TestRabbitMqConsumer  : consumer begin message -> {"userId":1}
2024-09-14 16:24:21.526 ERROR 53942 --- [tContainer#12-1] c.q.p.s.h.consumer.TestRabbitMqConsumer  : 消息消费失败,开始重试或入库人工介入 message -> {"userId":1}, errorMsg -> 模拟业务异常
2024-09-14 16:24:21.527 ERROR 53942 --- [tContainer#12-1] c.q.p.s.h.consumer.TestRabbitMqConsumer  : 消费失败,发布消息重试准备第1次重试 message ->{"userId":1}
2024-09-14 16:24:24.551  INFO 53942 --- [tContainer#12-1] c.q.p.s.h.consumer.TestRabbitMqConsumer  : consumer begin message -> {"userId":1}
2024-09-14 16:24:24.551 ERROR 53942 --- [tContainer#12-1] c.q.p.s.h.consumer.TestRabbitMqConsumer  : 消息消费失败,开始重试或入库人工介入 message -> {"userId":1}, errorMsg -> 模拟业务异常
2024-09-14 16:24:24.552 ERROR 53942 --- [tContainer#12-1] c.q.p.s.h.consumer.TestRabbitMqConsumer  : 消费失败,发布消息重试准备第1次重试 message ->{"userId":1}

消费者(Consumer) - 改进

/**
 * RabbitMq消费者
 * @author ChenYu ren
 * @date 2024/9/3
 */

@Slf4j
@Component
public class TestRabbitMqConsumer {

    @Resource
    private AmqpTemplate rabbitTemplate;

    @RabbitListener(queues = TestRabbitMqConfig.QUEUE,containerFactory = "testRabbitListenerContainerFactory")
    public void consumerMessage(Channel channel, List<Message> messageList) {
        messageList.forEach(message -> {
            String messageJson = new String(message.getBody(), StandardCharsets.UTF_8);
            try {
                log.info("consumer begin message -> {}",messageJson);
                //业务处理
                handleMessage(messageJson);
                // 消息处理完成ack
                channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            } catch (Exception e) {
                try {
                    log.error("消息消费失败,开始重试或入库人工介入 message -> {}, errorMsg -> {}",messageJson,e.getMessage());
                    Integer retryCount = (Integer) message.getMessageProperties().getHeaders().getOrDefault("x-retry-count", 0);
                    retryCount = retryCount + 1;
                    message.getMessageProperties().getHeaders().put("x-retry-count", retryCount);
                    channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
                    if (retryCount >= 3) {
                        //将消息发送的死信队列 | 入库报警人工介入
                        sendDeadQueueOrSaveLog(messageJson);
                    }else {
                        log.error("消费失败,发布消息重试准备第{}次重试 message ->{}",retryCount,messageJson);
                        //重试次数重置,消息发到队尾等待重新消费
                        rabbitTemplate.convertAndSend(TestRabbitMqConfig.EXCHANGE_NAME,TestRabbitMqConfig.ROUTING_KEY,message);
                    }
                }catch (Exception exception){
                    log.error("消息消费失败,重新发送消息失败,或入库失败 message -> {} errorMsg -> {}",messageJson,exception.getMessage());
                }
            }
        });
    }

    /**
     * 处理消息
     * @param message 消息
     */
    private void handleMessage(String message){
        JSONObject jsonObject = JSONObject.parseObject(message);
        if (jsonObject.getInteger("userId") == 1){
            throw new CustomException("模拟业务异常");
        }
        log.info("handleMessage consumer success message -> {} ",message);
    }

    /**
     * 消息消费重试次数达到最大上线,发送到死信队列或入库人工介入
     * @param message 消息
     */
    private void sendDeadQueueOrSaveLog(String message){
        log.info("消息消费失败,重试次数达到上限。写入死信队列或入库 成功 message -> {} ",message);
    }
}

输出:
2024-09-14 15:59:14.203  INFO 52966 --- [tContainer#12-1] c.q.p.s.h.consumer.TestRabbitMqConsumer  : consumer begin message -> {"userId":1}
2024-09-14 15:59:14.207 ERROR 52966 --- [tContainer#12-1] c.q.p.s.h.consumer.TestRabbitMqConsumer  : 消息消费失败,开始重试或入库人工介入 message -> {"userId":1}, errorMsg -> 模拟业务异常
2024-09-14 15:59:14.209 ERROR 52966 --- [tContainer#12-1] c.q.p.s.h.consumer.TestRabbitMqConsumer  : 消费失败,发布消息重试准备第1次重试 message ->{"userId":1}
2024-09-14 15:59:17.235  INFO 52966 --- [tContainer#12-1] c.q.p.s.h.consumer.TestRabbitMqConsumer  : consumer begin message -> {"userId":1}
2024-09-14 15:59:17.237 ERROR 52966 --- [tContainer#12-1] c.q.p.s.h.consumer.TestRabbitMqConsumer  : 消息消费失败,开始重试或入库人工介入 message -> {"userId":1}, errorMsg -> 模拟业务异常
2024-09-14 15:59:17.239 ERROR 52966 --- [tContainer#12-1] c.q.p.s.h.consumer.TestRabbitMqConsumer  : 消费失败,发布消息重试准备第2次重试 message ->{"userId":1}
2024-09-14 15:59:20.280  INFO 52966 --- [tContainer#12-1] c.q.p.s.h.consumer.TestRabbitMqConsumer  : consumer begin message -> {"userId":1}
2024-09-14 15:59:20.282 ERROR 52966 --- [tContainer#12-1] c.q.p.s.h.consumer.TestRabbitMqConsumer  : 消息消费失败,开始重试或入库人工介入 message -> {"userId":1}, errorMsg -> 模拟业务异常
2024-09-14 15:59:20.293  INFO 52966 --- [tContainer#12-1] c.q.p.s.h.consumer.TestRabbitMqConsumer  : 消息消费失败,重试次数达到上线。写入死信队列或入库 成功 message -> {"userId":1}