建立數據庫連接是昂貴操作:TCP 三次握手、SSL 握手、認證授權、會話初始化。頻繁創建/銷毀導致:應用響應延遲高、數據庫進程/線程耗盡、網絡端口耗盡 (TIME_WAIT)。連接池通過預創建、復用、生命周期管理 解決上述問題。
| 連接池 | 狀態 | 特點 | 推薦場景 |
|---|---|---|---|
| HikariCP | 🏆 事實標準 | 極致性能、輕量(~150KB)、零依賴、智能預發、JMX 監控 | 所有新項目、Spring Boot 2+ 默認 |
| Druid | 國內流行 | 強大監控、SQL 防火牆、日志統計、擴展性好 | 需詳細 SQL 審計、阿里系生態 |
| DBCP 2 | 維護中 | Tomcat 內置、JNDI 集成好、功能完整 | Tomcat/JNDI 環境、遷移舊項目 |
| C3P0 | ⚰️ 停止維護 | 性能差、Bug 多 | 勿用 |
建議:新項目首選 HikariCP。本文以 DBCP 2 為例講解通用配置原理,參數含義對照通用。
org.apache.commons
commons-dbcp2
2.12.0
org.apache.commons
commons-pool2
2.12.0
com.mysql
mysql-connector-j
8.3.0
# ===== 基礎連接信息 =====
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username=db_user
password=db_pass
# ===== 池大小控制 (核心) =====
# 最大活躍連接數:峰值併發上限。公式:CPU核心數 * (1 + 平均等待時間/平均服務時間)
# 經驗值:((core_count * 2) + effective_spindle_count) 或 簡單設為 20-50
maxTotal=50
# 最大空閒連接數:保持隨時可用的連接數,減少獲取延遲。建議 = maxTotal * 0.5~0.8
maxIdle=30
# 最小空閒連接數:啟動/低峰期保持的連接數,防止冷啟動延遲。建議 = maxIdle * 0.2~0.5
minIdle=10
# ===== 獲取連接超時 =====
# 從池獲取連接最大等待毫秒數。-1 無限等待(危險),建議 5000-30000
maxWaitMillis=10000
# ===== 連接驗證 (關鍵:防止「斷連」) =====
# 獲取連接時驗證:每次 borrow 都執行 validationQuery,性能損耗大但最安全
testOnBorrow=true
# 歸還連接時驗證:return 時檢查,分攤開銷
testOnReturn=false
# 空閒驗證:後台線程定期檢查空閒連接,核心機制
testWhileIdle=true
# 驗證查詢 SQL (MySQL 8.0+ 推薦)
validationQuery=SELECT 1
# 驗證查詢超時秒數
validationQueryTimeout=5
# 空閒驗證間隔:每 30 秒運行一次空閒連接檢查 (需 testWhileIdle=true)
timeBetweenEvictionRunsMillis=30000
# 連接在池中最小生存時間:超過此時間且空閒,才會被驗證器檢查 (防止剛創建就被檢)
minEvictableIdleTimeMillis=60000
# 每次驗證檢查的最大連接數 (-1 全部)
numTestsPerEvictionRun=-1
# ===== 棄用連接清理 (防泄漏) =====
# 啟用棄用連接移除:超時未歸還的連接強制回收
removeAbandonedOnBorrow=true
removeAbandonedOnMaintenance=true
# 棄用超時秒數:連接借出超過此時間視為棄用 (需 > 最大業務邏輯執行時間)
removeAbandonedTimeout=180
# 移除棄用連接時打印堆疊 (定位泄漏代碼位置)
logAbandoned=true
# ===== 其他優化 =====
# 初始化時創建的連接數
initialSize=10
# 預備語句緩存 (每連接緩存 PreparedStatement 數量)
poolPreparedStatements=true
maxOpenPreparedStatements=100
# 連接屬性 (傳給驅動)
connectionProperties=useUnicode=true;characterEncoding=utf8;cachePrepStmts=true;prepStmtCacheSize=250;prepStmtCacheSqlLimit=2048
public final class DataSourceManager {
private static final Logger log = LoggerFactory.getLogger(DataSourceManager.class);
private static volatile BasicDataSource dataSource;
private DataSourceManager() {}
public static BasicDataSource getDataSource() {
if (dataSource == null) {
synchronized (DataSourceManager.class) {
if (dataSource == null) {
initDataSource();
}
}
}
return dataSource;
}
private static void initDataSource() {
Properties props = new Properties();
try (InputStream is = DataSourceManager.class.getClassLoader().getResourceAsStream("dbcp.properties")) {
if (is == null) throw new IllegalStateException("dbcp.properties not found");
props.load(is);
} catch (IOException e) {
throw new ExceptionInInitializerError("Load dbcp.properties failed", e);
}
try {
dataSource = BasicDataSourceFactory.createDataSource(props);
// 啟動時預熱連接 (可選)
if (dataSource.getInitialSize() > 0) {
try (Connection conn = dataSource.getConnection()) {
log.info("連接池預熱成功,數據庫: {}", conn.getMetaData().getDatabaseProductName());
}
}
// 註冊 JVM 關閉鉤子優雅關閉
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try { dataSource.close(); log.info("連接池已關閉"); }
catch (SQLException e) { log.error("關閉連接池異常", e); }
}));
} catch (Exception e) {
throw new ExceptionInInitializerError("Init DBCP failed", e);
}
}
/** 獲取連接 (業務代碼調用) */
public static Connection getConnection() throws SQLException {
return getDataSource().getConnection();
}
/** 歸還連接 (try-with-resources 自動調用 close() 即歸還池) */
// 無需額外 closeConn 方法,直接 conn.close() 即可
}
public User findUserById(Long id) {
String sql = "SELECT id, name, email FROM users WHERE id = ?";
// try-with-resources 自動關閉 Connection/PreparedStatement/ResultSet
try (Connection conn = DataSourceManager.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
return user;
}
}
} catch (SQLException e) {
throw new DataAccessException("Query user failed", e);
}
return null;
}
公式:maxTotal = CPU核心數 * (1 + 平均等待時間 / 平均服務時間)
實測法:壓測逐步增大,觀察 QPS、RT、數據庫活躍會話數、CPU。
原則:寧小勿大。過大導致數據庫上下文切換劇增、鎖衝突、甚至 OOM。典型 8核機器 MySQL 設 50-100 足夠。
testOnBorrow=true:最安全,但每次獲取連接多一次網絡 RTT。高併發下成瓶頸。testOnBorrow=false, testWhileIdle=true, timeBetweenEvictionRunsMillis=30000。後台線程定期清理斷連,借用時無額外開銷。removeAbandonedOnBorrow=true, removeAbandonedTimeout=180, logAbandoned=true。超時強制回收並打印堆疊定位泄漏點。開啟 poolPreparedStatements=true, maxOpenPreparedStatements=100 並配合驅動端緩存 cachePrepStmts=true, prepStmtCacheSize=250,可大幅減少 SQL 解析開銷。
# HikariCP 核心配置 (application.yml)
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mydb?...
username: user
password: pass
hikari:
maximum-pool-size: 50
minimum-idle: 10
idle-timeout: 300000 # 5分鐘空閒回收
max-lifetime: 1800000 # 30分鐘強制換新
connection-timeout: 10000 # 獲取超時
validation-timeout: 5000
leak-detection-threshold: 60000 # 洩漏檢測閾值 60秒
connection-test-query: SELECT 1
pool-name: MyAppHikariPool
HikariCP 內部已優化:無鎖數組、智能預發、FastPath 獲取連接、最小化對象分配。同等配置下性能超越 DBCP 2 10-30%,且配置更簡單。
連接池是 Java 應用與數據庫的「生命線」。核心在於:合理的池大小、可靠的連接驗證、嚴格的泄漏防控、完善的監控告警。DBCP 2 配置參數全面但繁雜,新項目請無腦上 HikariCP。無論用哪個池,業務代碼必須遵守 try-with-resources 獲取/歸還連接,這是防泄漏的第一道防線。記住:連接池配置無銀彈,唯有壓測驗證、監控觀察、持續調優。