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
相关产品推荐
相关产品推荐

