針對上一節 DBUtility 存在的頻繁創建銷毀連接、無連接數上限、無復用機制三大痛點,本節引入 Apache Commons DBCP 2.x 實現生產級連接池,完成 ConnectionSource 重構與 EmpDAO 遷移。
| 痛點 | 無連接池後果 | 連接池解決 |
|---|---|---|
| 創建開銷大 | 每請求握手、認證、分配會話,延遲 10-50ms | 預熱連接,借用即用,亞毫秒級 |
| 連接數失控 | 併發衝擊擊穿 max_connections | 強制上限 maxActive,超時等待/拒絕 |
| 資源洩漏 | 異常路徑未關閉,連接泄漏導致不可用 | 借用/歸還機制,配合 removeAbandoned 強制回收 |
<dependencies>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version> <!-- 或 2.x: org.apache.commons:commons-dbcp2 -->
</dependency>
<dependency>
<groupId>commons-pool</groupId>
<artifactId>commons-pool</artifactId>
<version>1.6</version>
</dependency>
<!-- 部分版本需額外引入 -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version>
</dependency>
</dependencies>
db.propertiesjdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://127.0.0.1:3306/emp?useUnicode=true&characterEncoding=utf8 jdbc.user=wonderq jdbc.password=root # DBCP 連接池參數 dataSource.initialSize=10 # 初始化連接數 dataSource.minIdle=5 # 最小空閒連接 dataSource.maxIdle=20 # 最大空閒連接 dataSource.maxActive=50 # 最大活躍連接 (總上限) dataSource.maxWait=1000 # 獲取連接超時時間 (毫秒) dataSource.removeAbandoned=true # 自動回收棄用連接 dataSource.removeAbandonedTimeout=60 # 棄用判定秒數 dataSource.logAbandoned=true # 記錄棄用堆疊
package dao;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
import org.apache.commons.dbcp.BasicDataSource;
public class ConnectionSource {
private static BasicDataSource dataSource;
// 私有構造器,防實例化
private ConnectionSource() {}
// 雙重檢查鎖單例初始化
private static synchronized void init() {
if (dataSource != null) return;
Properties props = new Properties();
try (InputStream is = ConnectionSource.class.getClassLoader().getResourceAsStream("db.properties")) {
if (is == null) throw new RuntimeException("db.properties not found");
props.load(is);
BasicDataSource ds = new BasicDataSource();
ds.setDriverClassName(props.getProperty("jdbc.driver"));
ds.setUrl(props.getProperty("jdbc.url"));
ds.setUsername(props.getProperty("jdbc.user"));
ds.setPassword(props.getProperty("jdbc.password"));
// 讀取池參數 (提供默認值)
ds.setInitialSize(parseInt(props, "dataSource.initialSize", 10));
ds.setMinIdle(parseInt(props, "dataSource.minIdle", 5));
ds.setMaxIdle(parseInt(props, "dataSource.maxIdle", 20));
ds.setMaxWait(parseLong(props, "dataSource.maxWait", 1000));
String maxActive = props.getProperty("dataSource.maxActive");
if (maxActive != null && !"0".equals(maxActive.trim())) {
ds.setMaxActive(Integer.parseInt(maxActive));
}
// 棄用連接回收
ds.setRemoveAbandonedOnBorrow(true);
ds.setRemoveAbandonedTimeout(60);
ds.setLogAbandoned(true);
dataSource = ds;
} catch (IOException e) {
throw new RuntimeException("Load db.properties failed", e);
}
}
private static int parseInt(Properties p, String key, int def) {
String v = p.getProperty(key);
return (v != null) ? Integer.parseInt(v) : def;
}
private static long parseLong(Properties p, String key, long def) {
String v = p.getProperty(key);
return (v != null) ? Long.parseLong(v) : def;
}
// 獲取連接 (線程安全)
public static synchronized Connection getConnection() throws SQLException {
if (dataSource == null) init();
return dataSource.getConnection();
}
// 關閉數據源 (應用關閉時調用)
public static void shutdown() {
if (dataSource != null) {
try { dataSource.close(); } catch (SQLException e) { /* ignore */ }
dataSource = null;
}
}
}
package dao;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class EmpDAO {
public void findAll() {
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
conn = ConnectionSource.getConnection(); // 從池借用
stmt = conn.createStatement();
rs = stmt.executeQuery("SELECT empno, ename, sal, hiredate FROM emp");
while (rs.next()) {
System.out.printf("%d, %s, %.2f, %s%n",
rs.getInt("empno"),
rs.getString("ename"),
rs.getDouble("sal"),
rs.getDate("hiredate"));
}
} catch (SQLException e) {
throw new RuntimeException("數據庫訪問異常", e);
} finally {
// 關閉順序:RS -> Stmt -> Conn (歸還池)
if (rs != null) try { rs.close(); } catch (SQLException ignore) {}
if (stmt != null) try { stmt.close(); } catch (SQLException ignore) {}
if (conn != null) try { conn.close(); } catch (SQLException ignore) {}
}
}
public static void main(String[] args) {
new EmpDAO().findAll();
ConnectionSource.shutdown(); // 關閉池 (演示用,實際由容器管理)
}
}
輸出結果與直連模式完全一致,但底層已實現連接復用。可通過 JMX/管理端點監控池狀態:
NumActive: 1 NumIdle: 9 MaxActive: 50
| 參數 | 建議值 | 調優依據 |
|---|---|---|
| maxActive | CPU核數 × 2 ~ 4 | 避免過多連接導致上下文切換、鎖競爭 |
| initialSize / minIdle | 預期基礎併發量 | 預熱減少首次借用延遲 |
| maxWait | 1000-3000ms | 業務可容忍的最大等待時間 |
| testOnBorrow / validationQuery | true / SELECT 1 | 防火牆/網絡斷開導致的失效連接 |
| removeAbandoned | true | 防業務代碼忘記 close 洩漏 |
| timeBetweenEvictionRunsMillis | 30000-60000 | 空閒連接驅逐檢查間隔 |
# HikariCP 快速上手 HikariConfig config = new HikariConfig(); config.setJdbcUrl(url); config.setUsername(user); config.setPassword(pass); config.setMaximumPoolSize(20); HikariDataSource ds = new HikariDataSource(config); Connection conn = ds.getConnection();
引入 Apache DBCP 連接池,通過 ConnectionSource 封裝池生命周期,EmpDAO 無感遷移,徹底解決「頻繁創建、數量失控、洩漏無感」三大痛點。生產環境強烈建議升級 HikariCP,享受更高吞吐、更低延遲、更簡潔配置。