Java 應用與 Oracle 數據庫交互的兩大開銷:
高併發下頻繁 `new Connection()` + `createStatement()` 會導致 CPU 飆升、Library Cache 爭用、會話爆炸。
UCP 是 Oracle 11g+ 推薦的獨立於應用服務器的 JDBC 連接池實現,替代舊有 `OracleConnectionCacheImpl` / `Implicit Connection Cache`。
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ucp</artifactId>
<version>23.4.0.0</version> <!-- 或與 JDBC 驅動版本對齊 -->
</dependency>
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc11</artifactId>
<version>23.4.0.0</version>
</dependency>
| 參數 | Setter | 說明 | 建議值 |
|---|---|---|---|
| 初始連接數 | `setInitialPoolSize` | 池啟動時預創建連接數,預熱避免冷啟動延遲 | 等於 CPU 核心數 * 2 |
| 最小連接數 | `setMinPoolSize` | 閒置時保持的連接數,防止收縮過度 | 同 Initial |
| 最大連接數 | `setMaxPoolSize` | 池上限,防止數據庫會話耗盡 | 根據 DB `processes` 參數反推,留 20% 緩衝 |
| 連接獲取超時 | `setConnectionWaitTimeout` | 池滿等待毫秒數,超時拋 `PoolException` | 3000-5000ms |
| 空閒超時回收 | `setInactiveConnectionTimeout` | 閒置超過秒數自動關閉物理連接 (需 > 防火牆超時) | 300-600s |
| 存活檢測 | `setValidateConnectionOnBorrow` | 借用前執行 `SELECT 1 FROM DUAL` 驗證,防死連接 | true (關鍵) |
| SQL 執行超時 | `setQueryTimeout` | Statement 級查詢超時秒數 | 按業務 SLA 設定 |
import oracle.ucp.jdbc.PoolDataSource;
import oracle.ucp.jdbc.PoolDataSourceFactory;
import java.sql.Connection;
import java.sql.SQLException;
public class UcpPool {
private static final PoolDataSource PDS;
static {
try {
PDS = PoolDataSourceFactory.getPoolDataSource();
// 連接工廠
PDS.setConnectionFactoryClassName("oracle.jdbc.pool.OracleDataSource");
PDS.setURL("jdbc:oracle:thin:@//db-host:1521/pdb1");
PDS.setUser("app_user");
PDS.setPassword("secure_password");
// 池參數
PDS.setInitialPoolSize(10);
PDS.setMinPoolSize(10);
PDS.setMaxPoolSize(50);
PDS.setConnectionWaitTimeout(5); // 秒
PDS.setInactiveConnectionTimeout(300); // 秒
PDS.setValidateConnectionOnBorrow(true);
PDS.setSQLForValidateConnection("SELECT 1 FROM DUAL"); // 12.2+ 可省略
// 統計監控 (可選)
PDS.setConnectionPoolName("APP_MAIN_POOL");
} catch (SQLException e) {
throw new ExceptionInInitializerError("UCP 初始化失敗: " + e.getMessage());
}
}
public static Connection getConnection() throws SQLException {
return PDS.getConnection(); // 從池借用
}
public static void printStats() {
System.out.printf("可用:%d, 借用:%d, 總計:%d%n",
PDS.getAvailableConnectionsCount(),
PDS.getBorrowedConnectionsCount(),
PDS.getAvailableConnectionsCount() + PDS.getBorrowedConnectionsCount());
}
}
try (Connection conn = UcpPool.getConnection()) { // try-with-resources 自動 close() 歸還池
try (PreparedStatement ps = conn.prepareStatement("SELECT * FROM orders WHERE status=?")) {
ps.setString(1, "NEW");
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) { /* 處理 */ }
}
}
} // 連接自動歸還池,而非關閉物理連接
// 開啟隱式語句緩存 (連接屬性)
Properties props = new Properties();
props.setProperty("oracle.jdbc.implicitStatementCacheSize", "100"); // 緩存 100 條語句
props.setProperty("user", "app_user");
props.setProperty("password", "pwd");
Connection conn = DriverManager.getConnection(url, props);
// 使用模式:相同 SQL 文本 (含參數佔位符) 重複 prepareStatement
PreparedStatement ps = conn.prepareStatement("SELECT * FROM emp WHERE deptno=?"); // 首次硬解析
// ... 綁定參數、執行 ...
ps.close(); // 歸還客戶端緩存,**不關閉服務端游標**
// 後續相同 SQL
PreparedStatement ps2 = conn.prepareStatement("SELECT * FROM emp WHERE deptno=?"); // 從緩存獲取,軟解析
// ... 執行 ...
ps2.close();
// 獲取連接的物理連接代理 (OracleConnection)
oracle.jdbc.OracleConnection oraConn = conn.unwrap(oracle.jdbc.OracleConnection.class);
// 設置顯式緩存大小
oraConn.setStatementCacheSize(200);
// 顯式緩存/獲取
PreparedStatement ps = (PreparedStatement) oraConn.createStatementWithKey("GET_EMP_BY_DEPT");
if (ps == null) {
ps = conn.prepareStatement("SELECT * FROM emp WHERE deptno=?");
oraConn.setStatementWithKey("GET_EMP_BY_DEPT", ps); // 存入緩存
}
// 使用 ps ...
// 用完不 close(),而是歸還緩存 (或直接 close() 由驅動管理)
oraConn.closeWithKey("GET_EMP_BY_DEPT"); // 從緩存移除並關閉
-- 查看會話游標緩存命中率
SELECT name, value FROM v$sysstat WHERE name IN ('session cursor cache hits', 'session cursor cache count');
-- 查看特定 SQL 解析次數
SELECT sql_id, executions, parse_calls, loads, invalidations
FROM v$sqlarea
WHERE sql_text LIKE '%emp WHERE deptno=%';
-- parse_calls ≈ executions 表示硬解析多;parse_calls << executions 表示緩存命中良好
| 場景 | 原因 | 對策 |
|---|---|---|
| 僅執行一次的動態 SQL | 緩存開銷 (內存、管理) > 節省解析收益 | 不使用 `prepareStatement`,改用 `Statement` + 字符串拼接 (防注入) 或直接執行 |
| DDL 操作 (CREATE/ALTER/DROP) | DDL 隱式提交,導致游標失效,緩存無效 | 使用 `Statement.execute()` 直接執行 |
| 極大 SQL 文本 (> 100KB) | 客戶端緩存佔用過大 Heap | 調小緩存大小或禁用該連接緩存 |
| 長事務中持有大量緩存 Statement | 阻塞 Library Cache 中相關游標被淘汰/重用 | 盡早 `close()` 歸還緩存;避免長事務 |