數據庫連接池是高併發應用的基石。Tomcat 內建的 tomcat-dbcp (基於 Apache Commons DBCP 2 重新打包) 是輕量級、零依賴的優秀選擇。本文系統講解其配置參數、封裝實戰代碼、驗證方法及生產環境典型坑點。
無連接池痛點: 每次請求建立/銷毀 TCP 連接、TLS 握手、認證,延遲高、CPU 消耗大、數據庫最大連接數易耗盡。
連接池優勢: 預創建連接對象池化復用、借用歸還毫秒級、空閒連接驗證保活、動態擴縮容應對流量波峰。

# 基礎連接信息 dbname.Driver=com.mysql.cj.jdbc.Driver dbname.Url=jdbc:mysql://<ip>:3306/<dbname>?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&useSSL=false dbname.Username=<user> dbname.Password=<pwd> # 池核心參數 dbname.InitialSize=15 # 初始化創建連接數,預熱池 dbname.MinIdle=10 # 最小空閒連接數,低於此補充 dbname.MaxIdle=20 # 最大空閒連接數,超過此回收 dbname.MaxActive=50 # 最大活躍連接數 (總上限),防止壓垮 DB dbname.MaxWait=5000 # 獲取連接最大等待毫秒數,超時拋異常 # 連接驗證與保活 (關鍵!) dbname.ValidationQuery=SELECT 1 dbname.TestOnBorrow=true # 借用時驗證 (推薦開啟) dbname.TestOnReturn=false # 歸還時驗證 (通常關閉) dbname.TestWhileIdle=true # 空閒檢測線程驗證 dbname.TimeBetweenEvictionRunsMillis=30000 # 空閒檢測間隔 30s dbname.MinEvictableIdleTimeMillis=300000 # 空閒超過 5 分鐘可被驅逐 # MySQL 專用防斷連參數 (URL 中已包含) # autoReconnect=true&maxReconnects=3&connectTimeout=3000&socketTimeout=30000
max_connections 留 20% 緩衝。SELECT 1,Oracle 用 SELECT 1 FROM DUAL,PostgreSQL 用 SELECT 1。package com.example.util;
import org.apache.commons.dbcp2.BasicDataSource;
import javax.sql.DataSource;
import java.sql.*;
import java.util.concurrent.ConcurrentHashMap;
public abstract class DBUtil {
// 多數據源支持:Key=數據源名稱
private static final ConcurrentHashMap<String, DataSource> DS_MAP = new ConcurrentHashMap<>();
// 禁止外部實例化,強制繼承模式
protected DBUtil() {}
/** 子類實現:返回配置文件中的數據源前綴,如 "dbname" */
protected abstract String getDataSourceName();
/** 獲取/創建 DataSource (雙重檢查鎖單例) */
private DataSource getDataSource() {
String name = getDataSourceName();
return DS_MAP.computeIfAbsent(name, k -> createDataSource(k));
}
private BasicDataSource createDataSource(String prefix) {
BasicDataSource ds = new BasicDataSource();
// 讀取配置 (假設有 ConfigUtil 讀取 properties)
ds.setDriverClassName(ConfigUtil.getString(prefix + ".Driver"));
ds.setUrl(ConfigUtil.getString(prefix + ".Url"));
ds.setUsername(ConfigUtil.getString(prefix + ".Username"));
ds.setPassword(ConfigUtil.getString(prefix + ".Password"));
ds.setInitialSize(ConfigUtil.getInt(prefix + ".InitialSize", 10));
ds.setMinIdle(ConfigUtil.getInt(prefix + ".MinIdle", 5));
ds.setMaxIdle(ConfigUtil.getInt(prefix + ".MaxIdle", 20));
ds.setMaxTotal(ConfigUtil.getInt(prefix + ".MaxActive", 50)); // DBCP2 用 MaxTotal
ds.setMaxWaitMillis(ConfigUtil.getLong(prefix + ".MaxWait", 5000));
ds.setValidationQuery(ConfigUtil.getString(prefix + ".ValidationQuery", "SELECT 1"));
ds.setTestOnBorrow(ConfigUtil.getBoolean(prefix + ".TestOnBorrow", true));
ds.setTestWhileIdle(ConfigUtil.getBoolean(prefix + ".TestWhileIdle", true));
ds.setTimeBetweenEvictionRunsMillis(ConfigUtil.getLong(prefix + ".TimeBetweenEvictionRunsMillis", 30000));
ds.setMinEvictableIdleTimeMillis(ConfigUtil.getLong(prefix + ".MinEvictableIdleTimeMillis", 300000));
// 關鍵:移除棄用連接檢測 (DBCP2 特有)
ds.setRemoveAbandonedOnBorrow(true);
ds.setRemoveAbandonedTimeout(60); // 60s 未歸還視為棄用
ds.setLogAbandoned(true); // 記錄棄用堆疊,排查泄漏神器
return ds;
}
/** 獲取連接 (自動從池借用) */
protected Connection getConnection() throws SQLException {
return getDataSource().getConnection();
}
/** 模板方法:執行更新/插入/刪除 */
protected int executeUpdate(String sql, Object... params) throws SQLException {
try (Connection conn = getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
setParameters(ps, params);
return ps.executeUpdate();
}
}
/** 模板方法:查詢單條 */
protected <T> T queryOne(String sql, ResultSetMapper<T> mapper, Object... params) throws SQLException {
try (Connection conn = getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
setParameters(ps, params);
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? mapper.map(rs) : null;
}
}
}
/** 模板方法:查詢列表 */
protected <T> List<T> queryList(String sql, ResultSetMapper<T> mapper, Object... params) throws SQLException {
try (Connection conn = getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
setParameters(ps, params);
try (ResultSet rs = ps.executeQuery()) {
List<T> list = new ArrayList<>();
while (rs.next()) list.add(mapper.map(rs));
return list;
}
}
}
private void setParameters(PreparedStatement ps, Object... params) throws SQLException {
for (int i = 0; i < params.length; i++) {
ps.setObject(i + 1, params[i]);
}
}
@FunctionalInterface
public interface ResultSetMapper<T> {
T map(ResultSet rs) throws SQLException;
}
// 監控暴露:供 Actuator/Prometheus 採集
public static BasicDataSource getRawDataSource(String name) {
return (BasicDataSource) DS_MAP.get(name);
}
}
高併發運行數小時後,應用拋出 java.net.SocketException: Too many open files,數據庫連接數飆升至上限,新請求全部掛起。
// 錯誤代碼:非 static Map,每個 DAO 實例持有獨立 DataSource
public class UserDao extends DBUtil {
private Map<String, DataSource> dsMap = new HashMap<>(); // 每次 new UserDao() 都新建池!
// ...
}
核心錯誤: 連接池 必須是進程級單例。若 DAO 被 Spring 管理為 Prototype、或手動 new Dao() 且池容器非 static,每個請求/線程都會創建獨立連接池,導致文件描述符耗盡。
application.yml 配置 spring.datasource.type=org.apache.commons.dbcp2.BasicDataSource,注入 DataSource 或 JdbcTemplate,絕不手寫池創建代碼。DBUtil,用 static ConcurrentHashMap + computeIfAbsent 保證全局單例。setRemoveAbandonedOnBorrow(true) + setLogAbandoned(true),日誌會精確輸出未關閉連接的調用堆疊。# Spring Boot Actuator GET /actuator/metrics/hikaricp.connections.active # 若用 Hikari GET /actuator/metrics/dbcp2.connections.active # 需自定義 MeterBinder 導出 DBCP2 指標
# 1. 找到 Java 進程 PID ps aux | grep java | grep -v grep # 2. 查看該進程打開的網絡連接 (含數據庫) netstat -anp | grep <PID> | grep ESTABLISHED | wc -l # 3. 查看文件描述符使用 ls -l /proc/<PID>/fd | wc -l # 對比 ulimit -n (默認 1024 或 65535),若接近上限需調整 /etc/security/limits.conf
-- MySQL SHOW STATUS LIKE 'Threads_connected'; SHOW PROCESSLIST; -- 觀察 Sleep 狀態連接數
| 維度 | Tomcat-dbcp (DBCP2) | HikariCP |
|---|---|---|
| 效能 | 中等 (鎖競爭較大) | 極高 (無鎖設計、FastPath) |
| 記憶體佔用 | 較大 | 極小 (~130KB) |
| 配置複雜度 | 參數多、歷史包袱重 | 極簡 (核心參數 < 10) |
| 預設連接驗證 | 需手動開啟 TestOnBorrow | 內建輕量級驗證 |
| Spring Boot 預設 | 2.0 前預設 | 2.0+ 預設 |
結論: 維護舊專案用好 tomcat-dbcp (開啟棄用檢測、配置單例);新專案無腦上 HikariCP。
連接池看似簡單,實則是「資源管理、併發控制、故障熔斷」的綜合體。掌握核心參數邏輯、堅持單例模式、善用監控與棄用檢測,才能在生產環境穩如磐石。希望本文能成為你排查連接池疑難雜症的利器。