數據庫連接池是 Web 應用性能的關鍵基礎設施。本文詳細講解 Tomcat 容器級連接池配置(JNDI 模式)、Web 應用引用方式,以及 Spring 框架整合兩大主流模式,附帶生產環境調優參數與避坑指南。
| 無連接池 | 有連接池 |
|---|---|
| 每請求創建/銷毀連接,極其消耗資源與時間 | 預創建連接池,請求借用/歸還,毫秒級獲取 |
| 高併發下數據庫連接數失控,導致拒絕服務 | 最大連接數可控,防雪崩 |
| 連接洩漏難排查 | 統一生命週期管理,監控指標豐富 |
路徑:$CATALINA_BASE/conf/context.xml 或項目 META-INF/context.xml
<Context>
<!-- SQL Server 示例 -->
<Resource name="jdbc/news"
auth="Container"
type="javax.sql.DataSource"
maxTotal="100"
maxIdle="30"
maxWaitMillis="10000"
username="sa"
password="120010"
driverClassName="com.microsoft.sqlserver.jdbc.SQLServerDriver"
url="jdbc:sqlserver://localhost:1433;DatabaseName=news"
validationQuery="SELECT 1"
testOnBorrow="true"
testWhileIdle="true"
timeBetweenEvictionRunsMillis="60000"
minEvictableIdleTimeMillis="300000"
removeAbandonedOnBorrow="true"
removeAbandonedTimeout="60"
logAbandoned="true" />
<!-- Oracle 示例 -->
<Resource name="jdbc/oracle"
auth="Container"
type="javax.sql.DataSource"
maxTotal="100"
maxIdle="30"
maxWaitMillis="10000"
username="test"
password="bg"
driverClassName="oracle.jdbc.driver.OracleDriver"
url="jdbc:oracle:thin:@192.168.2.102:1521:orcl"
validationQuery="SELECT 1 FROM DUAL"
testOnBorrow="true" />
</Context>
| 參數 | 含義 | 建議值 |
|---|---|---|
maxTotal | 最大活躍連接數 | 100-200(視 DB 最大連接數與應用並發) |
maxIdle | 最大空閒連接數 | 30-50 |
maxWaitMillis | 獲取連接最大等待毫秒數 | 10000(10秒) |
validationQuery | 驗證連接有效性的 SQL | MySQL: SELECT 1; Oracle: SELECT 1 FROM DUAL; SQL Server: SELECT 1 |
testOnBorrow | 借用時驗證 | true(性能微損,換取穩定) |
testWhileIdle | 空閒驗證 | true |
timeBetweenEvictionRunsMillis | 空閒驗證間隔 | 60000(1分鐘) |
minEvictableIdleTimeMillis | 最小空閒驅逐時間 | 300000(5分鐘) |
removeAbandonedOnBorrow | 借用時回收棄用連接 | true(防洩漏) |
removeAbandonedTimeout | 棄用判定秒數 | 60-300 |
logAbandoned | 記錄棄用堆棧 | true(排查洩漏) |
mssql-jdbc.jar, ojdbc8.jar)放入 $CATALINA_HOME/lib/WEB-INF/lib/,否則類加載器隔離導致 ClassNotFoundException
<resource-ref>
<res-ref-name>jdbc/news</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>
res-ref-name 必須與 context.xml 中 name 一致(不含 jdbc/ 前綴也可,視容器實現)。
import javax.naming.*;
import javax.sql.DataSource;
import java.sql.Connection;
public class Dao {
private DataSource dataSource;
public Dao() throws NamingException {
Context ctx = new InitialContext();
// 標準 JNDI 名稱:java:comp/env/ + res-ref-name
dataSource = (DataSource) ctx.lookup("java:comp/env/jdbc/news");
}
public int countUsers() {
String sql = "SELECT COUNT(*) FROM users";
try (Connection conn = dataSource.getConnection()) {
return conn.createStatement().executeQuery(sql).getInt(1);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
<!-- applicationContext.xml -->
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="java:comp/env/jdbc/news" />
<property name="proxyInterface" value="javax.sql.DataSource" />
<property name="lookupOnStartup" value="true" />
<property name="cache" value="true" />
<property name="resourceRef" value="true" /> <!-- 自動補全 java:comp/env/ -->
</bean>
<!-- 事務管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 使用 HikariCP(Spring Boot 2+ 默認) -->
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
<property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver" />
<property name="jdbcUrl" value="jdbc:sqlserver://localhost:1433;DatabaseName=news" />
<property name="username" value="sa" />
<property name="password" value="120010" />
<property name="maximumPoolSize" value="100" />
<property name="minimumIdle" value="10" />
<property name="idleTimeout" value="300000" />
<property name="maxLifetime" value="1800000" />
<property name="connectionTimeout" value="10000" />
<property name="connectionTestQuery" value="SELECT 1" />
<property name="poolName" value="HikariPool-App" />
</bean>
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
url: jdbc:sqlserver://localhost:1433;DatabaseName=news
username: sa
password: 120010
hikari:
maximum-pool-size: 100
minimum-idle: 10
idle-timeout: 300000
max-lifetime: 1800000
connection-timeout: 10000
connection-test-query: SELECT 1
pool-name: HikariPool-App
jndi-name: java:comp/env/jdbc/news # 若部署至 Tomcat 且啟用 JNDI
testOnBorrow=true + validationQuery,防斷連removeAbandonedOnBorrow=true + logAbandoned=truenumActive, numIdle, numWaiting 到 Prometheus/GrafanamaxTotal ≤ DB max_connections - 預留,多應用共享 DB 需協調maxWaitMillis 略大於業務最大容忍延遲destroy-method="close" / Hikari close() 確保連接歸還| 現象 | 可能原因 | 排查命令/日誌 |
|---|---|---|
| 獲取連接超時 | 池耗盡/洩漏/DB 繁忙 | jmx 查看 numActive/numIdle、日誌搜尋 "abandoned" |
| Communications link failure | 連接被防火牆/DB 斷開 | 驗證 query + testWhileIdle + 減少 idleTimeout |
| ClassNotFoundException: Driver | 驅動包未放 Tomcat lib | 檢查 $CATALINA_HOME/lib/ |
| JNDI NameNotFoundException | resource-ref 名稱不匹配 | 對比 web.xml res-ref-name 與 context.xml name |
Tomcat 連接池配置核心三步:
java:comp/env/xxx 查找使用生產環境強烈建議:容器級池 + Spring JNDI 查找 或 Spring Boot + HikariCP 兩條成熟路徑。理解每個參數的業務含義,建立監控告警體系,才能在高併發下守住數據庫穩定性底線。