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
This commit is contained in:
parent
e9620eb07e
commit
d60b8eb01e
|
|
@ -5,29 +5,64 @@ import org.slf4j.Logger;
|
||||||
import vip.fubuki.playersync.config.JdbcConfig;
|
import vip.fubuki.playersync.config.JdbcConfig;
|
||||||
|
|
||||||
import java.sql.*;
|
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 {
|
public class JDBCsetUp {
|
||||||
|
|
||||||
private static final Logger LOGGER = LogUtils.getLogger();
|
private static final Logger LOGGER = LogUtils.getLogger();
|
||||||
|
|
||||||
/**
|
// Simple connection pool - reuses connections instead of opening new ones every query
|
||||||
* Returns a connection to the MySQL server.
|
private static final int POOL_SIZE = 5;
|
||||||
* @param selectDatabase if true, the returned URL includes the configured database name.
|
private static final LinkedBlockingQueue<Connection> connectionPool = new LinkedBlockingQueue<>(POOL_SIZE);
|
||||||
* @return a Connection object with the database explicitly selected.
|
private static String cachedUrl = null;
|
||||||
* @throws SQLException if a database access error occurs.
|
|
||||||
*/
|
private static String buildUrl(boolean selectDatabase) {
|
||||||
public static Connection getConnection(boolean selectDatabase) throws SQLException {
|
|
||||||
String dbName = JdbcConfig.DATABASE_NAME.get();
|
String dbName = JdbcConfig.DATABASE_NAME.get();
|
||||||
// Build the base URL
|
|
||||||
String url = "jdbc:mysql://" + JdbcConfig.HOST.get() + ":" + JdbcConfig.PORT.get();
|
String url = "jdbc:mysql://" + JdbcConfig.HOST.get() + ":" + JdbcConfig.PORT.get();
|
||||||
if (selectDatabase && !dbName.isEmpty()) {
|
if (selectDatabase && !dbName.isEmpty()) {
|
||||||
url += "/" + dbName;
|
url += "/" + dbName;
|
||||||
}
|
}
|
||||||
url += "?useUnicode=true&characterEncoding=utf-8&useSSL=" + JdbcConfig.USE_SSL.get()
|
url += "?useUnicode=true&characterEncoding=utf-8&useSSL=" + JdbcConfig.USE_SSL.get()
|
||||||
+ "&serverTimezone=UTC&allowPublicKeyRetrieval=true";
|
+ "&serverTimezone=UTC&allowPublicKeyRetrieval=true&autoReconnect=true";
|
||||||
Connection conn = DriverManager.getConnection(url, JdbcConfig.USERNAME.get(), JdbcConfig.PASSWORD.get());
|
return url;
|
||||||
// Ensure that the connection uses the desired database by explicitly issuing "USE dbName"
|
}
|
||||||
if (selectDatabase && !dbName.isEmpty()) {
|
|
||||||
|
/**
|
||||||
|
* 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()) {
|
try (Statement st = conn.createStatement()) {
|
||||||
st.execute("USE `" + dbName + "`");
|
st.execute("USE `" + dbName + "`");
|
||||||
}
|
}
|
||||||
|
|
@ -35,89 +70,102 @@ public class JDBCsetUp {
|
||||||
return conn;
|
return conn;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default connection always includes the database.
|
|
||||||
public static Connection getConnection() throws SQLException {
|
public static Connection getConnection() throws SQLException {
|
||||||
return getConnection(true);
|
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.
|
* Executes a query using a connection that includes the database.
|
||||||
*/
|
*/
|
||||||
public static QueryResult executeQuery(String sqlFormatString, Object... args) throws SQLException {
|
public static QueryResult executeQuery(String sqlFormatString, Object... args) throws SQLException {
|
||||||
String sql = String.format(sqlFormatString, args);
|
String sql = String.format(sqlFormatString, args);
|
||||||
LOGGER.trace(sql);
|
LOGGER.trace(sql);
|
||||||
Connection connection = getConnection(); // With database selected (and "USE" already run)
|
Connection connection = getConnection();
|
||||||
PreparedStatement queryStatement = connection.prepareStatement(sql);
|
PreparedStatement queryStatement = connection.prepareStatement(sql);
|
||||||
ResultSet resultSet = queryStatement.executeQuery();
|
ResultSet resultSet = queryStatement.executeQuery();
|
||||||
return new QueryResult(connection, queryStatement, resultSet);
|
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 {
|
private static void executeUpdate(boolean selectDatabase, String sqlFormatString, Object... args) throws SQLException {
|
||||||
String sql = String.format(sqlFormatString, args);
|
String sql = String.format(sqlFormatString, args);
|
||||||
LOGGER.trace(sql);
|
LOGGER.trace(sql);
|
||||||
try (Connection connection = getConnection()) { // With database selected
|
Connection connection = getConnection(selectDatabase);
|
||||||
try (PreparedStatement updateStatement = connection.prepareStatement(sql)) {
|
try (PreparedStatement updateStatement = connection.prepareStatement(sql)) {
|
||||||
updateStatement.executeUpdate();
|
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 {
|
public static void executeUpdate(String sqlFormatString, Object... args) throws SQLException {
|
||||||
executeUpdate(true, sqlFormatString, args);
|
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 {
|
public static void executeUpdate(String sql, int dummy) throws SQLException {
|
||||||
LOGGER.trace(sql);
|
LOGGER.trace(sql);
|
||||||
try (Connection connection = getConnection(false)) { // Without default database
|
try (Connection connection = getConnection(false);
|
||||||
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();
|
|
||||||
PreparedStatement updateStatement = connection.prepareStatement(sql)) {
|
PreparedStatement updateStatement = connection.prepareStatement(sql)) {
|
||||||
for (int i = 0; i < argument.length; i++) {
|
|
||||||
updateStatement.setString(i + 1, argument[i]);
|
|
||||||
}
|
|
||||||
updateStatement.executeUpdate();
|
updateStatement.executeUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static void update(String sql, String... argument) throws SQLException {
|
||||||
* Executes a parameterized update using PreparedStatement with proper escaping.
|
LOGGER.trace(sql);
|
||||||
* This prevents SQL injection and data corruption from special characters in values.
|
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 {
|
public static void executePreparedUpdate(String sql, Object... params) throws SQLException {
|
||||||
LOGGER.trace(sql);
|
LOGGER.trace(sql);
|
||||||
try (Connection connection = getConnection();
|
Connection connection = getConnection();
|
||||||
PreparedStatement stmt = connection.prepareStatement(sql)) {
|
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||||
for (int i = 0; i < params.length; i++) {
|
for (int i = 0; i < params.length; i++) {
|
||||||
stmt.setObject(i + 1, params[i]);
|
stmt.setObject(i + 1, params[i]);
|
||||||
}
|
}
|
||||||
stmt.executeUpdate();
|
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 {
|
public static QueryResult executePreparedQuery(String sql, Object... params) throws SQLException {
|
||||||
LOGGER.trace(sql);
|
LOGGER.trace(sql);
|
||||||
Connection connection = getConnection();
|
Connection connection = getConnection();
|
||||||
|
|
@ -129,32 +177,20 @@ public class JDBCsetUp {
|
||||||
return new QueryResult(connection, stmt, rs);
|
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
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
if (resultSet != null) {
|
if (resultSet != null) {
|
||||||
try {
|
try { resultSet.close(); } catch (SQLException e) { LOGGER.error("Error closing ResultSet", e); }
|
||||||
resultSet.close();
|
|
||||||
} catch (SQLException e) {
|
|
||||||
LOGGER.error("Error closing ResultSet", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preparedStatement != null) {
|
if (preparedStatement != null) {
|
||||||
try {
|
try { preparedStatement.close(); } catch (SQLException e) { LOGGER.error("Error closing PreparedStatement", e); }
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// Return connection to pool instead of closing
|
||||||
|
returnConnection(connection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user