JavaFX格斗游戏中用JDBC同步数据库如何降低内存消耗?
解决JavaFX格斗游戏JDBC持续查询内存飙升问题
核心问题:JDBC资源泄漏
你当前代码的内存飙升根本原因是每次查询都创建新的Statement和ResultSet,但没有关闭这些资源,JDBC驱动会持有这些资源的引用,导致内存无法被GC回收,循环次数越多内存占用越高。先把这个问题解决,内存就能稳定下来。
修复资源泄漏(必须做)
用Java的try-with-resources语法自动关闭Statement和ResultSet,避免手动关闭遗漏。同时不要直接返回ResultSet(因为它会随Statement关闭而失效),改成封装查询结果:
// 自定义一个简单类存储对手信息 class OpponentInfo { private final double x; private final double y; public OpponentInfo(double x, double y) { this.x = x; this.y = y; } public double getX() { return x; } public double getY() { return y; } } // 重构数据库查询方法 public OpponentInfo getOpponentInfo(int matchID, int opponentID) { String sql = "select xPos, YPos from matchDetails where matchID = ? and userID = ?"; // try-with-resources自动关闭PreparedStatement和ResultSet try (PreparedStatement stmt = connection.prepareStatement(sql)) { stmt.setInt(1, matchID); stmt.setInt(2, opponentID); try (ResultSet rs = stmt.executeQuery()) { if (rs.next()) { return new OpponentInfo(rs.getDouble("xPos"), rs.getDouble("YPos")); } } } catch (SQLException e) { e.printStackTrace(); } return null; } // 重构轮询线程 void startUpdatingOpponentInfo() { Thread updateThread = new Thread(() -> { while (matchID != -1) { OpponentInfo info = sqlConnection.getOpponentInfo(matchID, opponentID); if (info != null && info.getX() != -1 && info.getY() != -1) { // JavaFX UI更新必须在Application线程执行,用Platform.runLater包裹 Platform.runLater(() -> opponent.move(canvas, new Position(info.getX(), info.getY()))); } try { Thread.sleep(30); // 调整休眠时间,30ms足够满足格斗游戏的实时性 } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 正确处理中断 break; } } }); updateThread.setDaemon(true); // 设置为守护线程,游戏退出时自动销毁 updateThread.start(); }
进阶优化:从轮询改为被动通知
如果想彻底摆脱轮询的性能问题,可以让数据库主动推送变化,不用客户端反复查询。
方式1:监听MySQL Binlog
用mysql-binlog-connector-java库监听MySQL的Binlog日志,当matchDetails表的目标行有更新时,直接捕获变化并同步:
// 初始化Binlog客户端 BinaryLogClient binlogClient = new BinaryLogClient("你的MySQL地址", 3306, "用户名", "密码"); binlogClient.setServerId(1); // 唯一ID,避免和其他复制客户端冲突 binlogClient.setBinlogFilename("mysql-bin.000001"); // 可以自动获取最新Binlog binlogClient.setBinlogPosition(4); // 注册事件监听器 binlogClient.registerEventListener(event -> { if (event instanceof UpdateRowsEvent) { UpdateRowsEvent updateEvent = (UpdateRowsEvent) event; // 检查是否是matchDetails表的更新 if ("你的数据库名".equals(updateEvent.getTable().getSchema()) && "matchDetails".equals(updateEvent.getTable().getTable())) { for (Map.Entry<Serializable[], Serializable[]> entry : updateEvent.getRows()) { Serializable[] newRow = entry.getValue(); int rowMatchID = (int) newRow[0]; // 假设matchID是第一列 int rowUserID = (int) newRow[1]; // 假设userID是第二列 if (rowMatchID == matchID && rowUserID == opponentID) { double x = (double) newRow[2]; // 假设xPos是第三列 double y = (double) newRow[3]; // 假设YPos是第四列 // 同步到游戏UI Platform.runLater(() -> opponent.move(canvas, new Position(x, y))); } } } } }); // 启动监听线程 new Thread(() -> { try { binlogClient.connect(); } catch (IOException e) { e.printStackTrace(); } }).start();
注意:需要给MySQL用户授予REPLICATION SLAVE权限,并且MySQL的Binlog格式要设置为ROW模式(在my.cnf里配置binlog_format=ROW)。
方式2:用触发器+阻塞查询
创建MySQL触发器,当对手数据更新时写入临时通知表,客户端用阻塞查询等待新数据:
-- 创建通知表 CREATE TABLE match_change_notify ( match_id INT, user_id INT, x_pos DOUBLE, y_pos DOUBLE, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 创建更新触发器 DELIMITER // CREATE TRIGGER trigger_match_update AFTER UPDATE ON matchDetails FOR EACH ROW BEGIN -- 只有位置变化时才写入通知 IF OLD.xPos != NEW.xPos OR OLD.YPos != NEW.YPos THEN INSERT INTO match_change_notify (match_id, user_id, x_pos, y_pos) VALUES (NEW.matchID, NEW.userID, NEW.xPos, NEW.YPos); END IF; END // DELIMITER ;
客户端用SELECT ... FOR UPDATE WAIT实现阻塞查询(MySQL 8.0+支持):
void startUpdatingOpponentInfo() { Thread updateThread = new Thread(() -> { String sql = "SELECT x_pos, y_pos FROM match_change_notify WHERE match_id = ? AND user_id = ? AND create_time > ? FOR UPDATE WAIT 10"; Timestamp lastNotifyTime = new Timestamp(0); while (matchID != -1) { try (PreparedStatement stmt = connection.prepareStatement(sql)) { stmt.setInt(1, matchID); stmt.setInt(2, opponentID); stmt.setTimestamp(3, lastNotifyTime); try (ResultSet rs = stmt.executeQuery()) { if (rs.next()) { double x = rs.getDouble("x_pos"); double y = rs.getDouble("y_pos"); Platform.runLater(() -> opponent.move(canvas, new Position(x, y))); lastNotifyTime = rs.getTimestamp("create_time"); } } } catch (SQLException e) { e.printStackTrace(); } } }); updateThread.setDaemon(true); updateThread.start(); }
这种方式客户端只有在有数据变化时才会触发更新,没有变化时会阻塞等待,大幅降低查询频率和内存占用。
额外小技巧
- 只查必要字段:永远不要用
select *,只查询需要的xPos、YPos,减少数据传输和内存消耗。 - 调整休眠间隔:10ms的间隔太密集,格斗游戏60fps对应16ms左右的帧间隔,30ms的休眠完全能满足实时性,同时降低查询次数。
- 用PreparedStatement:预编译SQL可以复用数据库查询计划,比每次拼接字符串的性能更高,还能避免SQL注入。
内容的提问来源于stack exchange,提问作者antoo5_11
相关产品推荐
相关产品推荐

