RabbitMQ 在 Java 应用中内存溢出问题

一、Bug 场景

在一个基于 Java 的日志收集系统中,使用 RabbitMQ 作为消息队列来接收各个应用节点发送的日志消息。随着系统规模的扩大和业务量的增长,日志产生的频率和数据量不断增加。

二、代码示例

生产者代码(简化示例)

java 复制代码
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;

public class LogProducer {
    private static final String QUEUE_NAME = "log_queue";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            while (true) {
                String logMessage = generateLogMessage();
                channel.basicPublish("", QUEUE_NAME, null, logMessage.getBytes("UTF - 8"));
            }
        }
    }

    private static String generateLogMessage() {
        // 模拟生成日志消息,这里简单返回一个字符串
        return "This is a log message";
    }
}

消费者代码(简化示例)

java 复制代码
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import com.rabbitmq.client.AMQP;

import java.util.concurrent.TimeUnit;

public class LogConsumer {
    private static final String QUEUE_NAME = "log_queue";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        channel.basicConsume(QUEUE_NAME, true,
                "logConsumerTag",
                (consumerTag, delivery) -> {
                    String logMessage = new String(delivery.getBody(), "UTF - 8");
                    // 模拟复杂的日志处理逻辑,这里简单休眠一段时间
                    TimeUnit.SECONDS.sleep(1);
                    processLogMessage(logMessage);
                },
                consumerTag -> {
                    System.out.println("Consumer cancelled: " + consumerTag);
                });
    }

    private static void processLogMessage(String logMessage) {
        System.out.println("Processing log message: " + logMessage);
    }
}

三、问题描述

  1. 预期行为:生产者持续发送日志消息到 RabbitMQ 队列,消费者从队列中获取消息并进行处理,系统能够稳定运行,不会出现内存相关的错误。
  2. 实际行为 :经过一段时间运行后,消费者应用程序抛出 OutOfMemoryError 异常。这是因为消费者处理日志消息的速度较慢(这里通过 TimeUnit.SECONDS.sleep(1) 模拟复杂处理逻辑导致处理速度慢),而生产者发送消息的速度较快,导致 RabbitMQ 队列中的消息不断积压。随着积压消息数量的增加,RabbitMQ 会占用越来越多的内存来存储这些消息。当内存使用达到一定阈值时,RabbitMQ 可能会尝试将部分消息换页到磁盘,但如果换页操作频繁或者磁盘 I/O 性能不佳,再加上 Java 应用自身的内存使用,最终会导致整个 Java 应用出现内存溢出错误。

四、解决方案

  1. 提高消费者处理能力:优化消费者的日志处理逻辑,减少每条消息的处理时间,从而提高消费者从队列中获取和处理消息的速度,避免消息积压。例如,可以对复杂的日志处理逻辑进行异步化处理,或者采用多线程方式并行处理日志消息。

修改后的消费者代码(多线程处理)

java 复制代码
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import com.rabbitmq.client.AMQP;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class LogConsumer {
    private static final String QUEUE_NAME = "log_queue";
    private static final ExecutorService executor = Executors.newFixedThreadPool(10);

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        channel.basicConsume(QUEUE_NAME, true,
                "logConsumerTag",
                (consumerTag, delivery) -> {
                    String logMessage = new String(delivery.getBody(), "UTF - 8");
                    executor.submit(() -> processLogMessage(logMessage));
                },
                consumerTag -> {
                    System.out.println("Consumer cancelled: " + consumerTag);
                });
    }

    private static void processLogMessage(String logMessage) {
        System.out.println("Processing log message: " + logMessage);
    }
}
  1. 设置队列长度限制:在 RabbitMQ 中设置队列的最大长度,当队列达到最大长度时,生产者发送的新消息将被丢弃或者以其他方式处理(如发送到死信队列),防止队列无限增长导致内存耗尽。

修改后的生产者代码(设置队列参数)

java 复制代码
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.MessageProperties;

import java.util.HashMap;
import java.util.Map;

public class LogProducer {
    private static final String QUEUE_NAME = "log_queue";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            Map<String, Object> args = new HashMap<>();
            args.put("x-max-length", 1000); // 设置队列最大长度为 1000
            channel.queueDeclare(QUEUE_NAME, false, false, false, args);
            while (true) {
                String logMessage = generateLogMessage();
                channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, logMessage.getBytes("UTF - 8"));
            }
        }
    }

    private static String generateLogMessage() {
        // 模拟生成日志消息,这里简单返回一个字符串
        return "This is a log message";
    }
}
  1. 启用消息持久化并合理配置磁盘空间:将消息设置为持久化,使 RabbitMQ 在内存不足时可以更有效地将消息持久化到磁盘。同时,确保服务器有足够的磁盘空间来存储这些持久化的消息,并且优化磁盘 I/O 性能。

修改后的生产者代码(消息持久化)

java 复制代码
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.MessageProperties;

public class LogProducer {
    private static final String QUEUE_NAME = "log_queue";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            channel.queueDeclare(QUEUE_NAME, true, false, false, null);
            while (true) {
                String logMessage = generateLogMessage();
                channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, logMessage.getBytes("UTF - 8"));
            }
        }
    }

    private static String generateLogMessage() {
        // 模拟生成日志消息,这里简单返回一个字符串
        return "This is a log message";
    }
}

通过以上方法,可以有效避免因 RabbitMQ 消息积压导致 Java 应用出现内存溢出的问题,确保系统的稳定运行。

相关推荐
geovindu33 分钟前
java: Facade Pattern
java·开发语言·后端·外观模式·结构型模式
listening77734 分钟前
HarmonyOS 6.1 元服务深度优化:从“秒开”到“常驻”的极致体验
java·开发语言·spring
AC赳赳老秦1 小时前
司法公开数据采集应用:OpenClaw 抓取裁判文书公开信息,批量整理同类参考案例
java·大数据·python·数据挖掘·数据分析·php·openclaw
笑小枫1 小时前
用 Claude Code 推翻重写笑小枫网站
java·人工智能·spring boot·ai编程
霸道流氓气质1 小时前
ApiPost 中配置自动获取 Token 并调用业务接口完整指南
java·服务器·数据库
独隅2 小时前
IntelliJ IDEA 接入多种AI大模型插件终极指南(2026.1 企业合规版)
java·人工智能·intellij-idea
还是奇怪2 小时前
Simon Willison 用 DSPy 优化 Datasette Agent 提示词:提示工程正在变成可测试的软件工程
java·开发语言·软件工程
zfoo-framework2 小时前
1.ansible安装 2.虚拟机克隆
java
码上解惑2 小时前
从 Dify 工作流说起:常用节点怎么选、怎样组合?
java·人工智能·ai·agent·dify·智能体·spring ai
青山木3 小时前
Hot 100 --- 岛屿数量
java·数据结构·算法·leetcode·深度优先·广度优先