背景
資料庫連線池的概念大家應該都很熟悉,類似dbcp、c3p0,還有如今常用的druid等,使用時無非就是從網上抄幾段配置,調一下引數,但是深入理解資料庫連線池的原理在如今的Java開發中還是很有必要的,這裡以dbcp為例,簡要分析一下資料庫連線池的實現原理
這裡我採用的版本是2.5.0版本
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.5.0</version>
</dependency>
複製程式碼
分析
準備
我們先來看一段原生jdbc程式碼(省略了異常捕獲):
Class.forName("com.mysql.cj.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/xxxxxx";
Connection conn = DriverManager.getConnection(url, "user", "password");
Statement stat = conn.createStatement();
ResultSet set = stat.executeQuery(sql);
while(set.next()) {
System.out.println(set.getString(2));
}
stat.close();
conn.close();
複製程式碼
程式碼結構這裡不關心,重點是其中的一句
Connection conn = DriverManager.getConnection(url, "user", "password");
複製程式碼
為什麼要單獨拿這一條出來說呢?因為我們使用資料庫連線池也是呼叫了getConnection方法,在JDBC 2.0 API中,Java提倡使用DateSource介面來獲取連線
我們現在的重點就是找到dhcp提供的DataSource介面,如果我們使用過dhcp配置,就一定見過這樣的配置
spring:
datasource:
type: org.apache.commons.dbcp2.BasicDataSource
複製程式碼
我們就以BasicDataSource作為突破點,這個類實現了DataSource介面:
public class BasicDataSource implements DataSource, BasicDataSourceMXBean, MBeanRegistration, AutoCloseable
複製程式碼
既然我們要探究原理,就不要關注細枝末節的部分,我們直接點進最核心的getConnection方法,同時為了程式碼結構清晰,這裡忽略所有異常捕獲語句:
@Override
public Connection getConnection() throws SQLException {
if (Utils.IS_SECURITY_ENABLED) {
final PrivilegedExceptionAction<Connection> action = new PaGetConnection();
return AccessController.doPrivileged(action);
}
return createDataSource().getConnection();
}
複製程式碼
整段程式碼很簡單,根據是否開啟安全管理,來選擇獲取連線的方式,我們這裡只看沒有安全管理的方式
資料來源的建立
我們先來看createDataSource方法:
protected DataSource createDataSource() throws SQLException {
if (closed) {
throw new SQLException("Data source is closed");
}
// 如果資料來源已存在,就直接返回
if (dataSource != null) {
return dataSource;
}
synchronized (this) {
// 使用雙檢鎖的設計,保證dataSource是單例的
if (dataSource != null) {
return dataSource;
}
// 註冊MBean,這裡不是重點,可以暫時不關心
jmxRegister();
// 建立返回原生連線的工廠
final ConnectionFactory driverConnectionFactory = createConnectionFactory();
// 建立連線池工廠
boolean success = false;
PoolableConnectionFactory poolableConnectionFactory;
try {
poolableConnectionFactory = createPoolableConnectionFactory(driverConnectionFactory);
poolableConnectionFactory.setPoolStatements(poolPreparedStatements);
poolableConnectionFactory.setMaxOpenPreparedStatements(maxOpenPreparedStatements);
success = true;
} catch (final SQLException se) {
throw se;
} catch (final RuntimeException rte) {
throw rte;
} catch (final Exception ex) {
throw new SQLException("Error creating connection factory", ex);
}
if (success) {
// 建立連線池成功
createConnectionPool(poolableConnectionFactory);
}
// 建立資料來源池來管理連線
DataSource newDataSource;
success = false;
try {
newDataSource = createDataSourceInstance();
newDataSource.setLogWriter(logWriter);
success = true;
} catch (final SQLException se) {
throw se;
} catch (final RuntimeException rte) {
throw rte;
} catch (final Exception ex) {
throw new SQLException("Error creating datasource", ex);
} finally {
if (!success) {
closeConnectionPool();
}
}
// 如果初始化連線數大於0,就進行預載入
try {
for (int i = 0; i < initialSize; i++) {
connectionPool.addObject();
}
} catch (final Exception e) {
closeConnectionPool();
throw new SQLException("Error preloading the connection pool", e);
}
// 如果空間連線回收器執行間隔時間大於0,則新增回收器任務
startPoolMaintenance();
// 返回資料來源
dataSource = newDataSource;
return dataSource;
}
}
複製程式碼
程式碼看似很長,其實流程很簡單:
- 判斷能不能直接返回結果(資料來源關閉或已存在),如果能,就直接返回
- 建立返回資料庫連線的工廠
- 建立連線池工廠
- 用資料庫連線工廠來包裝連線池工廠
- 建立資料庫連線池
- 通過連線池來建立資料來源例項
- 根據設定的引數,判斷是否要進行額外操作(比如預載入資料庫連線)
- 返回資料來源
我們重點只有三個:資料庫連線工廠是什麼,連線池工廠是什麼,連線池是什麼。我們先來看資料庫連線工廠
createConnectionFactory
這個方法的程式碼相比於上面的又有些混亂了,為了助於理解,我這裡還是忽略了所有的異常捕獲程式碼段:
protected ConnectionFactory createConnectionFactory() throws SQLException {
// 載入JDBC驅動
Driver driverToUse = this.driver;
if (driverToUse == null) {
Class<?> driverFromCCL = null;
if (driverClassName != null) {
if (driverClassLoader == null) {
driverFromCCL = Class.forName(driverClassName);
} else {
driverFromCCL = Class.forName(driverClassName, true, driverClassLoader);
}
}
if (driverFromCCL == null) {
// 這裡的url就是我們與資料庫建立連線的url
driverToUse = DriverManager.getDriver(url);
} else {
driverToUse = (Driver) driverFromCCL.getConstructor().newInstance();
if (!driverToUse.acceptsURL(url)) {
throw new SQLException("No suitable driver", "08001");
}
}
}
// 設定連線
final String user = userName;
if (user != null) {
connectionProperties.put("user", user);
} else {
log("DBCP DataSource configured without a 'username'");
}
final String pwd = password;
if (pwd != null) {
connectionProperties.put("password", pwd);
} else {
log("DBCP DataSource configured without a 'password'");
}
final ConnectionFactory driverConnectionFactory = new DriverConnectionFactory(driverToUse, url,
connectionProperties);
return driverConnectionFactory;
}
複製程式碼
這個方法做的事情很簡單,就是單純的載入驅動,真正建立連線工廠的是倒數第二行new DriverConnectionFactory這段程式碼,最終我們建立的也是DriverConnectionFactory型別物件,這裡我們向這個工廠物件傳入了資料庫驅動、連線url、使用者名稱和密碼等屬性
關於DriverConnectionFactory工廠類,這裡就列一段其中的程式碼,我相信其他就不用多講了
@Override
public Connection createConnection() throws SQLException {
return driver.connect(connectionString, properties);
}
複製程式碼
PoolableConnectionFactory
接下來看這個連線池工廠類,但是我們並不準備先研究這個類,而是回到createDataSource方法中,發現有一個createConnectionPool方法,傳入了PoolableConnectionFactory類物件,我們進入這個方法中:
protected void createConnectionPool(final PoolableConnectionFactory factory) {
// 建立一個物件池
final GenericObjectPoolConfig<PoolableConnection> config = new GenericObjectPoolConfig<>();
updateJmxName(config);
config.setJmxEnabled(registeredJmxObjectName != null);
final GenericObjectPool<PoolableConnection> gop = createObjectPool(factory, config, abandonedConfig);
gop.setMaxTotal(maxTotal);
gop.setMaxIdle(maxIdle);
gop.setMinIdle(minIdle);
gop.setMaxWaitMillis(maxWaitMillis);
gop.setTestOnCreate(testOnCreate);
gop.setTestOnBorrow(testOnBorrow);
gop.setTestOnReturn(testOnReturn);
gop.setNumTestsPerEvictionRun(numTestsPerEvictionRun);
gop.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
gop.setSoftMinEvictableIdleTimeMillis(softMinEvictableIdleTimeMillis);
gop.setTestWhileIdle(testWhileIdle);
gop.setLifo(lifo);
gop.setSwallowedExceptionListener(new SwallowedExceptionLogger(log, logExpiredConnections));
gop.setEvictionPolicyClassName(evictionPolicyClassName);
factory.setPool(gop);
connectionPool = gop;
}
複製程式碼
- 第一行建立了一個物件池,Apache提供了四種物件池,GenericObjectPoolConfig只是其中一種,我們在本篇中暫且不關心其實現細節,只需要明白它是一個物件池就夠了
- 接下來兩行與Jmx有關,這裡暫且不關心
- 接下來數行,通過我們提供的連線池和其他配置來建立一個物件池,同時設定變數的值,這些值都可以通過我們的配置檔案來設定
- 最後我們再把連線池設定到PoolableConnectionFactory工廠類中,並將資料來源本身的連線池設定為該連線池
現在明白我們為什麼不直接研究PoolableConnectionFactory了吧,因為實際上我們呼叫的連線池是connectionPool
獲取連線
剛才我們介紹了了連線工廠、連線池工廠和連線池,為了防止你忘記接下來要幹什麼,這裡再把我們一開始研究的程式碼段放上來:
return createDataSource().getConnection();
複製程式碼
剛才只是研究了createDataSource方法,這個方法最終返回了一個資料來源物件(這個資料來源物件是通過connectionPool包裝而來的PoolingDataSource<PoolableConnection>物件),我們接下來就要看真正獲取連線的方法getConnection
這是一個抽象方法,因為createDataSource返回的是PoolingDataSource物件,所以我們就從這個物件中來分析,為了讓程式碼結構清晰,這裡依然略去了異常捕獲程式碼:
@Override
public Connection getConnection() throws SQLException {
final C conn = pool.borrowObject();
if (conn == null) {
return null;
}
return new PoolGuardConnectionWrapper<>(conn);
}
複製程式碼
我們先解決兩個可能會有的問題:
- C是Connection子類
- PoolGuardConnectionWrapper是為了確保被關閉的連線不能被重用的包裝連線類
現在應該就沒有問題了,整個流程就是獲取連線,如果獲取不到就返回空,否則返回一個包裝類。連線通過borrowObject方法來獲取,這個方法的實現是在GenericObjectPool中(不知道為什麼是這個類的一定是前面的程式碼沒有認真看),關於這個類的實現方法就不是本篇的重點,在以後我會單獨寫一篇文章來介紹Apache提供的四種物件池
總結
最後依照慣例,來總結一下DBCP連線池獲取連線的步驟,這裡以呼叫getConnection方法獲取連線為例分析:
- 判斷資料來源是否關閉或已存在,如果關閉則丟擲異常,如果已存在則直接返回
- 建立可以返回資料庫連線的工廠類,並載入資料庫驅動
- 通過資料庫連線工廠類,來建立一個連線池工廠類
- 通過連線池工廠類,建立資料庫連線池並設定屬性
- 通過連線池來建立資料來源,並根據配置的屬性來進行一些初始化操作
- 通過資料來源中的物件池來獲取物件(連線)