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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 15:40:06