一站式了解RocketMQ如何解决消息堆积问题😗

引言

先前我们讲过了RocketMQ是如何解决顺序消息问题的,今天我们讲一下RocketMQ是如何解决消息堆积问题的。每种MQ解决消息堆积问题的策略可能都不一样,但发生消息堆积问题的原因大致一样,即生产者生产消息的速率与消费者消费消息的速率不匹配,导致消息大量堆积在队列中。

调整消费者线程池大小🤓

通过增加消费者的并发度,可以加快消息的消费速度。这可以通过修改消费者配置中的线程池大小参数实现,通过增加线程池中的线程数,可以让消费者并行处理更多的消息,从而加快整体的消费速度。

RocketMQ 的 Java 客户端中,使用 MessageListenerConcurrently 接口监听消息,其底层是线程池驱动的。我们可以通过设置 consumeThreadMinconsumeThreadMax 来控制线程池的最小和最大线程数

java 复制代码
public class ConsumerExample {
    public static void main(String[] args) throws MQClientException {
        // 创建消费者实例,并指定消费者组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("example_consumer_group");

        // 设置 NameServer 地址
        consumer.setNamesrvAddr("127.0.0.1:9876");

        // 设置线程池参数:最小线程数和最大线程数
        consumer.setConsumeThreadMin(20);  // 默认是 10
        consumer.setConsumeThreadMax(64);  // 默认是 64

        // 订阅 Topic 和 Tag(* 表示订阅所有 Tag)
        consumer.subscribe("TestTopic", "*");

        // 注册消息监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                for (MessageExt msg : msgs) {
                    System.out.println("收到消息:" + new String(msg.getBody()));
                    // 模拟业务处理耗时
                    try {
                        Thread.sleep(100);  // 假设每条消息处理需要 100ms
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        // 启动消费者
        consumer.start();

        System.out.println("消费者已启动");
    }
}

需要注意的点:

  • 如果你的消息处理逻辑比较轻量(如只是记录日志),可以保持默认。
  • 如果消息处理比较重(比如涉及数据库操作、远程调用等),则应该适当增大线程池
  • 注意不要超过系统资源限制(CPU、内存、网络带宽等),否则可能引发资源竞争或OOM问题。

增加消费者实例数量😶‍🌫️

既然生产者生产速率过快,那我们就提高消费者的消费速率,来匹配生产者的生产速率。这个通常是解决消息堆积最简单粗暴的方法,也就是水平扩展增加消费者实例数量,大白话来说就是加机器。

增加消费者实例的数量,以便能够并行处理更多的消息。确保消费者组(Consumer Group)下的消费者实例数量不超过主题(Topic)的队列数量,以避免资源浪费。

检查消费端消费逻辑😣

假如线上检测到消费者端消费速率过慢,第一时间不应该想到去加机器,而是应该先去想到是不是消费者消费出错了,导致过多的消息重试。

java 复制代码
public class OrderConsumer {
    public static void main(String[] args) throws Exception {
        // 创建消费者实例,并指定消费者组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("order_consumer_group");

        // 设置 NameServer 地址
        consumer.setNamesrvAddr("127.0.0.1:9876");

        // 订阅 Topic 和 Tag(* 表示订阅所有 Tag)
        consumer.subscribe("OrderTopic", "*");

        // 注册消息监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                for (MessageExt msg : msgs) {
                    try {
                        // 模拟业务处理逻辑
                        processOrder(msg);
                    } catch (Exception e) {
                        // 如果处理失败,返回 CONSUME_SUCCESS 会导致消息被丢弃
                        // 应该返回 RECONSUME_LATER 让消息稍后重试
                        return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                    }
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        // 启动消费者
        consumer.start();

        System.out.println("消费者已启动");
    }

    private static void processOrder(MessageExt msg) throws Exception {
        // 模拟复杂的业务处理逻辑
        Thread.sleep(500);  // 假设每条消息处理需要 500ms
        if (Math.random() < 0.1) {
            throw new RuntimeException("模拟处理失败");
        }
    }
}

异常处理不当

在上面的代码中,如果 processOrder 方法抛出异常,当前实现会直接返回 RECONSUME_LATER,导致消息不断重试。这可能会导致性能下降和资源浪费。

改进方法:

  • 添加重试次数限制,避免无限重试。
  • 将频繁失败的消息转移到死信队列进行后续处理。
java 复制代码
consumer.registerMessageListener(new MessageListenerConcurrently() {
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
        for (MessageExt msg : msgs) {
            int reconsumeTimes = msg.getReconsumeTimes();
            if (reconsumeTimes >= 3) {  // 重试超过3次,放入死信队列
                System.out.println("消息重试超过3次,放入死信队列:" + new String(msg.getBody()));
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
            try {
                processOrder(msg);
            } catch (Exception e) {
                System.out.println("消息处理失败,将稍后重试:" + new String(msg.getBody()));
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;
            }
        }
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
});

业务处理耗时过长

如果 processOrder 方法中的业务处理逻辑非常耗时,会导致消费者无法及时处理新的消息,从而造成消息堆积。

改进方法:

  • 优化业务处理逻辑,减少不必要的计算和IO操作。
  • 使用异步处理方式,将耗时操作放到单独的线程池中执行。
java 复制代码
private static ExecutorService executor = Executors.newFixedThreadPool(10);

private static void processOrderAsync(MessageExt msg) {
    executor.submit(() -> {
        try {
            processOrder(msg);
        } catch (Exception e) {
            System.out.println("异步处理失败:" + new String(msg.getBody()));
        }
    });
}

consumer.registerMessageListener(new MessageListenerConcurrently() {
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
        for (MessageExt msg : msgs) {
            processOrderAsync(msg);
        }
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
});

批量消费🥱

批量消费消息 是一种非常有效的优化手段,特别适用于处理大量消息堆积的场景。通过批量消费,消费者可以在一次拉取中获取多条消息,并以批次为单位进行处理,从而减少网络开销和提高整体吞吐量

java 复制代码
public class BatchConsumerExample {
    public static void main(String[] args) throws MQClientException {
        // 创建消费者实例,并指定消费者组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("order_consumer_group");

        // 设置 NameServer 地址
        consumer.setNamesrvAddr("127.0.0.1:9876");

        // 设置每次最多消费的消息数量(默认是 1)
        consumer.setConsumeMessageBatchMaxSize(10);

        // 订阅 Topic 和 Tag(* 表示订阅所有 Tag)
        consumer.subscribe("OrderTopic", "*");

        // 注册监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                System.out.println("开始消费一批消息,共 " + msgs.size() + " 条");

                for (MessageExt msg : msgs) {
                    System.out.println("收到订单消息:" + new String(msg.getBody()));
                    // 模拟业务逻辑处理
                    try {
                        Thread.sleep(5);  // 假设每条消息处理耗时 5ms
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                System.out.println("本批次消息处理完成");
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        // 启动消费者
        consumer.start();

        System.out.println("消费者已启动,准备批量消费消息...");
    }
}
  • consumeMessageBatchMaxSize 控制的是实际传给业务逻辑的批次大小
  • pullBatchSize 控制的是从 Broker 拉取的消息数 ,应大于等于 consumeMessageBatchMaxSize

总结❤️

解决消息堆积问题常用方法就是上面说的这些啦,当然从RocketMQ仪表盘监测消息消费情况也是很重要的

如果你看了这篇文章有收获可以点赞+关注+收藏🤩,这是对笔者更新的最大鼓励!如果你有更多方案或者文章中有错漏之处,请在评论区提出帮助笔者勘误,祝你拿到更好的offer!

相关推荐
MO2T4 分钟前
使用 Flask 构建基于 Dify 的企业资金投向与客户分类评估系统
后端·python·语言模型·flask
光溯星河12 分钟前
【实践手记】Git重写已提交代码历史信息
后端·github
PetterHillWater30 分钟前
Trae中实现OOP原则工程重构
后端·aigc
圆滚滚肉肉33 分钟前
后端MVC(控制器与动作方法的关系)
后端·c#·asp.net·mvc
SimonKing34 分钟前
拯救大文件上传:一文彻底彻底搞懂秒传、断点续传以及分片上传
java·后端·架构
深栈解码34 分钟前
JUC并发编程 内存布局和对象头
java·后端
37手游后端团队36 分钟前
巧妙利用装饰器模式给WebSocket连接新增持久化
后端
编程乐趣39 分钟前
C#版本LINQ增强开源库
后端
tonydf39 分钟前
记一次近6万多个文件的备份过程
windows·后端
前端付豪40 分钟前
13、你还在 print 调试🧾?教你写出自己的日志系统
后端·python