From d60b8eb01e2f5f7e99eba57a05eb7fecc77dcf84 Mon Sep 17 00:00:00 2001 From: laforetbrut Date: Thu, 26 Mar 2026 21:13:17 +0100 Subject: [PATCH] Add connection pool - fix 10% server thread usage from MySQL connects Spark showed PlayerSync consuming 10.16% of the server thread, almost entirely from DriverManager.getConnection() (TCP handshake + MySQL auth + USE db) called for EVERY single query. With auto-save every 60s, each player generated ~6 new connections per save cycle on main thread. FIX: Simple connection pool (LinkedBlockingQueue, 5 connections). - Connections are reused instead of opened/closed per query - isValid(2) check before reuse to detect dead connections - returnConnection() puts connections back in pool instead of closing - QueryResult.close() also returns to pool - autoReconnect=true in JDBC URL for resilience - shutdownPool() for clean server stop - Non-database connections (startup DDL) bypass the pool Expected improvement: ~90% reduction in MySQL overhead on server thread. Vyrriox --- .../vip/fubuki/playersync/util/JDBCsetUp.java | 182 +++++++++++------- 1 file changed, 109 insertions(+), 73 deletions(-) diff --git a/src/main/java/vip/fubuki/playersync/util/JDBCsetUp.java b/src/main/java/vip/fubuki/playersync/util/JDBCsetUp.java index 26d6bc6..e1a9134 100644 --- a/src/main/java/vip/fubuki/playersync/util/JDBCsetUp.java +++ b/src/main/java/vip/fubuki/playersync/util/JDBCsetUp.java @@ -5,29 +5,64 @@ import org.slf4j.Logger; import vip.fubuki.playersync.config.JdbcConfig; import java.sql.*; +import java.util.concurrent.LinkedBlockingQueue; +/** + * JDBC utility with a simple connection pool. + * Previously, every single query opened a NEW MySQL connection (TCP handshake + auth + USE db), + * consuming ~10% of server thread time. Now connections are pooled and reused. + */ public class JDBCsetUp { private static final Logger LOGGER = LogUtils.getLogger(); - /** - * Returns a connection to the MySQL server. - * @param selectDatabase if true, the returned URL includes the configured database name. - * @return a Connection object with the database explicitly selected. - * @throws SQLException if a database access error occurs. - */ - public static Connection getConnection(boolean selectDatabase) throws SQLException { + // Simple connection pool - reuses connections instead of opening new ones every query + private static final int POOL_SIZE = 5; + private static final LinkedBlockingQueue connectionPool = new LinkedBlockingQueue<>(POOL_SIZE); + private static String cachedUrl = null; + + private static String buildUrl(boolean selectDatabase) { String dbName = JdbcConfig.DATABASE_NAME.get(); - // Build the base URL String url = "jdbc:mysql://" + JdbcConfig.HOST.get() + ":" + JdbcConfig.PORT.get(); if (selectDatabase && !dbName.isEmpty()) { url += "/" + dbName; } url += "?useUnicode=true&characterEncoding=utf-8&useSSL=" + JdbcConfig.USE_SSL.get() - + "&serverTimezone=UTC&allowPublicKeyRetrieval=true"; - Connection conn = DriverManager.getConnection(url, JdbcConfig.USERNAME.get(), JdbcConfig.PASSWORD.get()); - // Ensure that the connection uses the desired database by explicitly issuing "USE dbName" - if (selectDatabase && !dbName.isEmpty()) { + + "&serverTimezone=UTC&allowPublicKeyRetrieval=true&autoReconnect=true"; + return url; + } + + /** + * Gets a connection from the pool, or creates a new one if pool is empty. + * Connections are validated before returning (checks if still alive). + */ + public static Connection getConnection(boolean selectDatabase) throws SQLException { + // For non-default-database connections (startup DDL), always create fresh + if (!selectDatabase) { + return DriverManager.getConnection(buildUrl(false), JdbcConfig.USERNAME.get(), JdbcConfig.PASSWORD.get()); + } + + // Try to get a pooled connection + Connection conn = connectionPool.poll(); + if (conn != null) { + try { + if (!conn.isClosed() && conn.isValid(2)) { + return conn; + } + // Connection is dead, close it and create new + conn.close(); + } catch (SQLException e) { + // Connection is broken, ignore and create new + } + } + + // Create a new connection + if (cachedUrl == null) { + cachedUrl = buildUrl(true); + } + conn = DriverManager.getConnection(cachedUrl, JdbcConfig.USERNAME.get(), JdbcConfig.PASSWORD.get()); + String dbName = JdbcConfig.DATABASE_NAME.get(); + if (!dbName.isEmpty()) { try (Statement st = conn.createStatement()) { st.execute("USE `" + dbName + "`"); } @@ -35,89 +70,102 @@ public class JDBCsetUp { return conn; } - // Default connection always includes the database. public static Connection getConnection() throws SQLException { return getConnection(true); } + /** + * Returns a connection to the pool instead of closing it. + * If the pool is full, the connection is closed normally. + */ + private static void returnConnection(Connection conn) { + if (conn == null) return; + try { + if (conn.isClosed()) return; + if (!connectionPool.offer(conn)) { + // Pool is full, close the connection + conn.close(); + } + } catch (SQLException e) { + try { conn.close(); } catch (SQLException ignored) {} + } + } + + /** + * Shuts down the pool, closing all connections. + */ + public static void shutdownPool() { + Connection conn; + while ((conn = connectionPool.poll()) != null) { + try { conn.close(); } catch (SQLException ignored) {} + } + } + /** * Executes a query using a connection that includes the database. */ public static QueryResult executeQuery(String sqlFormatString, Object... args) throws SQLException { String sql = String.format(sqlFormatString, args); LOGGER.trace(sql); - Connection connection = getConnection(); // With database selected (and "USE" already run) + Connection connection = getConnection(); PreparedStatement queryStatement = connection.prepareStatement(sql); ResultSet resultSet = queryStatement.executeQuery(); return new QueryResult(connection, queryStatement, resultSet); } - /** - * Executes an update using a connection with or without the database within the JDBC URL - */ private static void executeUpdate(boolean selectDatabase, String sqlFormatString, Object... args) throws SQLException { String sql = String.format(sqlFormatString, args); LOGGER.trace(sql); - try (Connection connection = getConnection()) { // With database selected - try (PreparedStatement updateStatement = connection.prepareStatement(sql)) { - updateStatement.executeUpdate(); + Connection connection = getConnection(selectDatabase); + try (PreparedStatement updateStatement = connection.prepareStatement(sql)) { + updateStatement.executeUpdate(); + } finally { + if (selectDatabase) { + returnConnection(connection); + } else { + connection.close(); } } } - /** - * Executes an update using a connection that includes the database in the JDBC URL - */ public static void executeUpdate(String sqlFormatString, Object... args) throws SQLException { executeUpdate(true, sqlFormatString, args); } - /** - * Executes an update using a connection that does NOT include a default database. - * This method is used for commands like "CREATE DATABASE IF NOT EXISTS ..." - */ public static void executeUpdate(String sql, int dummy) throws SQLException { LOGGER.trace(sql); - try (Connection connection = getConnection(false)) { // Without default database - try (PreparedStatement updateStatement = connection.prepareStatement(sql)) { - updateStatement.executeUpdate(); - } - } - } - - /** - * A helper method for updates with parameters. - */ - public static void update(String sql, String... argument) throws SQLException { - LOGGER.trace(sql); - try (Connection connection = getConnection(); + try (Connection connection = getConnection(false); PreparedStatement updateStatement = connection.prepareStatement(sql)) { - for (int i = 0; i < argument.length; i++) { - updateStatement.setString(i + 1, argument[i]); - } updateStatement.executeUpdate(); } } - /** - * Executes a parameterized update using PreparedStatement with proper escaping. - * This prevents SQL injection and data corruption from special characters in values. - */ + public static void update(String sql, String... argument) throws SQLException { + LOGGER.trace(sql); + Connection connection = getConnection(); + try (PreparedStatement updateStatement = connection.prepareStatement(sql)) { + for (int i = 0; i < argument.length; i++) { + updateStatement.setString(i + 1, argument[i]); + } + updateStatement.executeUpdate(); + } finally { + returnConnection(connection); + } + } + public static void executePreparedUpdate(String sql, Object... params) throws SQLException { LOGGER.trace(sql); - try (Connection connection = getConnection(); - PreparedStatement stmt = connection.prepareStatement(sql)) { + Connection connection = getConnection(); + try (PreparedStatement stmt = connection.prepareStatement(sql)) { for (int i = 0; i < params.length; i++) { stmt.setObject(i + 1, params[i]); } stmt.executeUpdate(); + } finally { + returnConnection(connection); } } - /** - * Executes a parameterized query using PreparedStatement with proper escaping. - * Caller MUST close the returned QueryResult (use try-with-resources). - */ public static QueryResult executePreparedQuery(String sql, Object... params) throws SQLException { LOGGER.trace(sql); Connection connection = getConnection(); @@ -129,32 +177,20 @@ public class JDBCsetUp { return new QueryResult(connection, stmt, rs); } - public record QueryResult(Connection connection,PreparedStatement preparedStatement, ResultSet resultSet) implements AutoCloseable { + /** + * QueryResult now returns the connection to the pool on close instead of closing it. + */ + public record QueryResult(Connection connection, PreparedStatement preparedStatement, ResultSet resultSet) implements AutoCloseable { @Override public void close() { if (resultSet != null) { - try { - resultSet.close(); - } catch (SQLException e) { - LOGGER.error("Error closing ResultSet", e); - } + try { resultSet.close(); } catch (SQLException e) { LOGGER.error("Error closing ResultSet", e); } } - if (preparedStatement != null) { - try { - preparedStatement.close(); - } catch (SQLException e) { - LOGGER.error("Error closing PreparedStatement", e); - } - } - - if (connection != null) { - try { - connection.close(); - } catch (SQLException e) { - LOGGER.error("Error closing Connection", e); - } + try { preparedStatement.close(); } catch (SQLException e) { LOGGER.error("Error closing PreparedStatement", e); } } + // Return connection to pool instead of closing + returnConnection(connection); } } }