You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JFreeChart百万级实时数据绘图优化咨询:8小时无卡顿存历史数据

超大规模实时数据绘图优化方案(8小时全量数据支持)

针对每秒1000条数据、连续8小时(总计2880万条)实时绘图且保留全量历史数据的需求,结合现有代码,从渲染优化、数据库读取、内存管理三个核心方向给出具体优化方案:


一、核心优化:数据分层渲染与动态加载

现有代码直接将全量数据放入dataset用于渲染,这是30万点后卡顿的核心原因。JFreeChart无法高效渲染百万级以上的点,必须实现**「全量数据后台存储+可见区域动态加载+数据降采样」**的分层机制:

1. 分离全量存储与渲染数据集

  • 保留fullSeriesMap存储所有历史数据(可继续用XYSeries,或替换为更高效的自定义时间序列结构)
  • dataset仅保留当前坐标轴可见范围内的数据,且根据视图缩放级别自动降采样
  • 监听坐标轴范围变化,实时从fullSeriesMap提取数据更新渲染数据集:
    // 给DateAxis添加范围变化监听器
    domainAxis.addChangeListener(new AxisChangeListener() {
        @Override
        public void axisChanged(AxisChangeEvent event) {
            SwingUtilities.invokeLater(() -> updateVisibleData());
        }
    });
    
    // 动态更新可见区域数据
    private void updateVisibleData() {
        DateAxis domainAxis = (DateAxis) plot.getDomainAxis();
        double lower = domainAxis.getLowerBound();
        double upper = domainAxis.getUpperBound();
    
        // 遍历所有通道
        for (Map.Entry<Integer, XYSeries> entry : fullSeriesMap.entrySet()) {
            int channel = entry.getKey();
            XYSeries fullSeries = entry.getValue();
            XYSeries renderSeries = seriesMap.get(channel);
    
            // 清空当前渲染数据
            renderSeries.clear();
    
            // 提取可见区域内的数据,根据缩放级别决定是否降采样
            long visiblePoints = (long)((upper - lower) / 1); // 假设原始数据间隔1ms
            if (visiblePoints > 10000) { // 当可见点超过10000时启用降采样
                downsampleAndAdd(fullSeries, renderSeries, lower, upper);
            } else {
                // 直接添加可见区域的所有点
                for (int i = 0; i < fullSeries.getItemCount(); i++) {
                    double x = fullSeries.getX(i);
                    if (x >= lower && x <= upper) {
                        renderSeries.add(x, fullSeries.getY(i));
                    }
                }
            }
        }
    }
    
    // 降采样实现:按时间窗口合并数据(示例取窗口平均值,可改为最大值/最小值)
    private void downsampleAndAdd(XYSeries fullSeries, XYSeries renderSeries, double lower, double upper) {
        double windowSize = (upper - lower) / 5000; // 固定渲染5000个点,平衡清晰度与性能
        double currentWindowStart = lower;
        double sum = 0;
        int count = 0;
    
        for (int i = 0; i < fullSeries.getItemCount(); i++) {
            double x = fullSeries.getX(i);
            double y = fullSeries.getY(i);
    
            if (x < lower) continue;
            if (x > upper) break;
    
            if (x <= currentWindowStart + windowSize) {
                sum += y;
                count++;
            } else {
                // 添加窗口数据
                renderSeries.add(currentWindowStart + windowSize/2, sum/count);
                // 重置窗口
                currentWindowStart += windowSize;
                sum = y;
                count = 1;
            }
        }
        // 添加最后一个窗口
        if (count > 0) {
            renderSeries.add(currentWindowStart + windowSize/2, sum/count);
        }
    }
    

2. 关闭不必要的渲染特性

  • 禁用线条渲染器的形状绘制(仅保留线条),减少渲染开销:
    XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer();
    renderer.setBaseShapesVisible(false); // 关闭点形状
    renderer.setBaseShapesFilled(false);
    plot.setRenderer(renderer);
    

二、数据库读取性能优化

现有代码的数据库操作存在连接复用差、分页效率低、EDT线程阻塞等问题,优化如下:

1. 使用JDBC连接池

替换每次新建连接的方式,用HikariCP等高性能连接池,减少连接创建开销:

// 全局初始化连接池(单例)
private static HikariDataSource dataSource;
static {
    HikariConfig config = new HikariConfig();
    config.setJdbcUrl(DB_URL);
    config.setUsername(USER);
    config.setPassword(PASSWORD);
    config.setMaximumPoolSize(10); // 根据服务器性能调整
    dataSource = new HikariDataSource(config);
}

// 后续获取连接改为:
Connection connection = dataSource.getConnection();

2. 优化历史数据加载(替代OFFSET分页)

MySQL的OFFSET在大偏移量时会扫描大量冗余数据,改用WHERE id < lastLoadedId的方式分页:

private void loadRemainingData() {
    int batchSize = 30000;
    long lastLoadedId = lastProcessedId; // 初始为初始加载的最后ID

    while (true) {
        String query = "SELECT id, channel_N, data_value, control_dev_time FROM data_response WHERE id < ? ORDER BY id DESC LIMIT ?";
        try (Connection connection = dataSource.getConnection();
             PreparedStatement preparedStatement = connection.prepareStatement(query)) {

            preparedStatement.setLong(1, lastLoadedId);
            preparedStatement.setInt(2, batchSize);
            try (ResultSet resultSet = preparedStatement.executeQuery()) {
                if (!resultSet.next()) {
                    break;
                }

                do {
                    int id = resultSet.getInt("id");
                    int channelN = resultSet.getInt("channel_N");
                    double dataValue = resultSet.getDouble("data_value");
                    long controlDevTime = resultSet.getLong("control_dev_time");

                    long millis = TimeConverter.convertNanoTimeToMillis(controlDevTime);
                    fullSeriesMap.get(channelN).addOrUpdate(millis, dataValue);
                    lastLoadedId = id;
                } while (resultSet.next());
            }
        } catch (SQLException e) {
            e.printStackTrace();
            break;
        }
    }
    System.out.println("Remaining data loaded.");
}

3. 避免EDT线程执行数据库操作

现有updateData方法将数据库查询放在SwingUtilities.invokeLater中,会阻塞UI线程。改为后台线程查询,仅更新UI时使用invokeLater:

private static void updateData() {
    Executors.newSingleThreadExecutor().submit(() -> {
        String query = "SELECT id, channel_N, data_value, control_dev_time FROM data_response WHERE id > ? ORDER BY id ASC";
        try (Connection connection = dataSource.getConnection();
             PreparedStatement preparedStatement = connection.prepareStatement(query)) {

            preparedStatement.setInt(1, lastProcessedId);
            try (ResultSet resultSet = preparedStatement.executeQuery()) {
                List<DataPoint> newPoints = new ArrayList<>();
                while (resultSet.next()) {
                    int id = resultSet.getInt("id");
                    int channelN = resultSet.getInt("channel_N");
                    double dataValue = resultSet.getDouble("data_value");
                    long controlDevTime = resultSet.getLong("control_dev_time");
                    long millis = TimeConverter.convertNanoTimeToMillis(controlDevTime);
                    newPoints.add(new DataPoint(id, channelN, millis, dataValue));
                    lastProcessedId = id;
                }

                // 仅更新UI时使用invokeLater
                SwingUtilities.invokeLater(() -> {
                    DateAxis domainAxis = (DateAxis) plot.getDomainAxis();
                    double lower = domainAxis.getLowerBound();
                    double upper = domainAxis.getUpperBound();
                    
                    for (DataPoint point : newPoints) {
                        fullSeriesMap.get(point.channelN).addOrUpdate(point.millis, point.value);
                        // 如果当前点在可见范围内,同步更新渲染数据集
                        if (point.millis >= lower && point.millis <= upper) {
                            seriesMap.get(point.channelN).addOrUpdate(point.millis, point.value);
                        }
                    }
                });
                System.out.println("Fetched new data. Last processed ID: " + lastProcessedId);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    });
}

// 辅助类存储数据点
private static class DataPoint {
    int id;
    int channelN;
    long millis;
    double value;

    public DataPoint(int id, int channelN, long millis, double value) {
        this.id = id;
        this.channelN = channelN;
        this.millis = millis;
        this.value = value;
    }
}

三、内存与定时器优化

1. 内存优化:使用高效的时间序列存储

如果XYSeries在千万级数据下内存占用过高,可以考虑使用第三方高效时间序列库(如Chronicle Queue)或自定义分段存储,将历史数据按时间分段保存,减少单对象内存压力。

2. 替换定时器为SwingTimer(可选)

用javax.swing.Timer代替java.util.Timer,更适配Swing UI线程模型,注意定时器任务仅触发后台查询,不要在任务内做耗时操作:

Timer timer = new Timer(FREQUENCY, e -> updateData());
timer.start();

四、数据库端索引优化

确保data_response表的id字段为主键,并给control_dev_time添加索引,加快时间范围查询:

CREATE INDEX idx_control_dev_time ON data_response(control_dev_time);

内容的提问来源于stack exchange,提问作者IlyaK

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 17:45:53