要執(zhí)行sql語句就要有db連接,mybatis的db連接也從DataSource獲取,mybatis實現(xiàn)了UNPOOLED和POOLED兩類數(shù)據(jù)源。
一、DataSorce的創(chuàng)建和使用
在解析配置文件是時根據(jù)指定的配置生產(chǎn)對應(yīng)的dataSorce,生成的dataSorce保存在configuration的environment中。配置文件被解析為cconfiguration,每個DataSourceFactory都通過其唯一的Cconfiguration來創(chuàng)建的,environment就是配置文件中的environments配置。
// src/main/java/org/apache/ibatis/session/Configuration.java
public class Configuration {
protected Environment environment;
}
// src/main/java/org/apache/ibatis/mapping/Environment.java
public final class Environment {
private final DataSource dataSource;
}
1.1 DataSorce實例生成
environments配置中的dataSource的type屬性指定了生成其dataSource實例的工廠類DataSourceFactory,通過對應(yīng)的DataSourceFactory來生產(chǎn)特定的DataSource。
DataSourceFactory接口只有兩個方法:
// src/main/java/org/apache/ibatis/datasource/DataSourceFactory.java
public interface DataSourceFactory {
// 設(shè)置DataSource的屬性,配置文件中的屬性便是通過該方法設(shè)置的
void setProperties(Properties props);
// 獲取一個DataSource
DataSource getDataSource();
}
在解析配置時根據(jù)的配置的type屬性DataSourceFactory類型并生成其實例。
private DataSourceFactory dataSourceElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type");
// 獲取dataSource配置的屬性
Properties props = context.getChildrenAsProperties();
// 創(chuàng)建DataSourceFactory
DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance();
// 為DataSourceFactory的DataSource設(shè)置屬性
factory.setProperties(props);
return factory;
}
throw new BuilderException("Environment declaration requires a DataSourceFactory.");
}
每個SqlSession都有一個用來執(zhí)行sql的執(zhí)行器Executor,Executor的事務(wù)管理器JdbcTransaction會持用Environment中的dataSource。
// src/main/java/org/apache/ibatis/session/defaults/DefaultSqlSessionFactory.java
public class DefaultSqlSessionFactory implements SqlSessionFactory {
// 生成SqlSession
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// 生成事務(wù)管理器并將dataSource給到事務(wù)管理器
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 生成sqlSession的事務(wù)管理器
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
}
}
在事務(wù)管理器中定義了獲取db連接的接口,mybatis提供的兩個事務(wù)管理器JDBC和MANAGED,兩個類型對該接口的實現(xiàn)方式都差不多,都是在getConnection是判斷其connection屬性是否為null,若不為null則通過其dataSource獲取到一個并給到connection屬性。
// src/main/java/org/apache/ibatis/transaction/Transaction.java
public interface Transaction {
//獲取db連接
Connection getConnection() throws SQLException;
}
// src/main/java/org/apache/ibatis/transaction/jdbc/JdbcTransaction.java
public class JdbcTransaction implements Transaction {
protected Connection connection;
protected DataSource dataSource;
// 連接存在直接返回連接
@Override
public Connection getConnection() throws SQLException {
if (connection == null) {
openConnection();
}
return connection;
}
// 從dataSource中獲取連接
protected void openConnection() throws SQLException {
connection = dataSource.getConnection();
// ...
}
}
1.2 什么時候會獲取連接
在真正要對db執(zhí)行一臺語句的時候會去從事務(wù)管理器中去獲取連接,來生成執(zhí)行sql的Statement。
sqlSesson中的語句是通過其Exector來執(zhí)行的,在各個類型的Executor中,對于所以的語句只要不是從緩存可以獲取的或是語句的Statement有緩存,都會先從事務(wù)管理其中獲取到連接去生產(chǎn)執(zhí)行語句的Statement。
在所有Executor的父類BaseExecutor中定義了獲取連接:
// src/main/java/org/apache/ibatis/executor/BatchExecutor.java
protected Connection getConnection(Log statementLog) throws SQLException {
Connection connection = transaction.getConnection();
if (statementLog.isDebugEnabled()) {
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
return connection;
}
}
1.3 什么時候釋放連接
連接的釋放依賴sqlSesson的關(guān)閉,也就是在關(guān)閉sqlSession時關(guān)閉連接。
連接是一個緊缺而重要的資源并且sqlSession也不是線程安全的,所以sqlSession在使用完成后要及時的關(guān)閉。mybatis官方甚至建議sqlSesion的生命周期應(yīng)該是請求內(nèi)的。
二、unpooled數(shù)據(jù)源
mybatis提供了unpooled、POOLED和JNDI三種類型的數(shù)據(jù)源:
- unpooled:每次想數(shù)據(jù)源獲取數(shù)據(jù)時都會新建一個連接,在關(guān)閉是也會真正的將連接關(guān)閉:
- POOLED:池化的數(shù)據(jù)源
- JNDI:使用外部提供的數(shù)據(jù)源。
unpooled比較直接簡單,每次獲取連接時都新建一條連接,關(guān)閉sqlSesion時直接將其關(guān)閉。
// 獲取連接
private Connection doGetConnection(Properties properties) throws SQLException {
initializeDriver();
Connection connection = DriverManager.getConnection(url, properties);
configureConnection(connection);
return connection;
}
三、POOLED數(shù)據(jù)源
pooled數(shù)據(jù)源是池化的連接。
3.1 POOLEN連接池配置項
poolMaximumActiveConnections
最大活動連接數(shù),即被某個線程占用在使用的連接,默認(rèn)為10。
poolMaximumIdleConnections
最大空間連接數(shù),即未被任何線程使用的連接,默認(rèn)為5。
poolMaximumCheckoutTime
活躍連接被收回給其它線程使用前的等待時間,即活躍連接超過了該時間則有可能被給到其它線程使用,單位為毫秒,默認(rèn)為20000。
poolTimeToWait
當(dāng)需要連接到空閑連接為空同時活躍連接也到達(dá)了最大并且做‘老’的活躍連接也未超時時,獲取連接的動作要等待poolTimeToWait時間后再重新嘗試獲取,單位為毫秒,默認(rèn)為20000。
poolMaximumLocalBadConnectionTolerance
獲取到連接可能不能用了,這就是一個環(huán)連接,但獲取到一個環(huán)連接時會嘗試從新獲取,但一個線程一次獲取連接時獲取到的壞連接次數(shù)不能超過poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance,默認(rèn)為3。
poolPingEnabled
檢測連接是否可用時,是否開啟ping測試,這里ping測試是指通過使用該連接執(zhí)行一個sql看是否可用成功執(zhí)行。若執(zhí)行成功則連接可用,若執(zhí)行拋出任何異常連接都不可用。默認(rèn)為“NO PING QUERY SET”,執(zhí)行該語句一定會報錯,所以該配置必須配。
poolPingQuery
執(zhí)行ping測試時執(zhí)行的sql,默認(rèn)為false。
poolPingConnectionsNotUsedFor
poolPingConnectionsNotUsedFor執(zhí)行ping測試頻率,默認(rèn)為0即每次測試連接連接性的同時都進(jìn)行ping測試。
3.2 POOLED數(shù)據(jù)源簡介
POOLED數(shù)據(jù)源的連接池又分為空閑連接池和活動連接池,初始時這兩個池都是空的。獲取連接的基本步驟如下:
- 先從空閑連接池中獲取,若空閑連接池中有空閑的連接則返回。
- 若空閑連接池中沒有連接但活動連接池沒滿,則新建一個新的連接并放到活躍連接池中然后放回。
- 若空閑連接池中為空且活躍池也滿了,則嘗試從活躍連接池檢出第一個連接,當(dāng)然該連接可能當(dāng)前并沒有過期(沒有超過poolMaximumCheckoutTime),那么就等待poolTimeToWait時間后重復(fù)到第一步。

當(dāng)關(guān)閉連接時,對POOLED的連接并不會釋放,而是放回到空閑鏈接池中。
我們可以看到POOLED數(shù)據(jù)源的的瓶頸主要還是在于活躍連接池的大小,活躍連接池的數(shù)量決定了當(dāng)前最多有多少連接可以在同一時段內(nèi)訪問db,對于db訪問密集的應(yīng)用來說該數(shù)量甚至決定了單個應(yīng)用能同時支持的并發(fā)數(shù)。
同時db是在用過后才會放到空閑連接池中,并不會預(yù)先創(chuàng)建,這樣避免了持有過多無用的連接,但這要求我們連接要避免慢查詢并且一定要及時關(guān)閉連接。
上面只是連接池的基本邏輯,詳細(xì)實現(xiàn)如下。
3.3 PoolState
PoolState記錄了池的狀態(tài),主要有以下屬性:
- accumulatedRequestTime:獲取鏈接累計耗時
- requestCount: 向鏈接池請求獲取鏈接的累次次數(shù)
- badConnectionCount:累計獲取到的壞鏈接數(shù)
- claimedOverdueConnectionCount :從活躍鏈接池中累計獲取鏈接的數(shù)
- hadToWaitCount:等待連接過期的等待次數(shù) 活躍鏈接被從獲取鏈接池中移除時累計存在的時間
- idleConnections:空閑連接池
protected final List<PooledConnection> idleConnections = new ArrayList<>();
- activeConnections:活躍連接池
protected final List<PooledConnection> activeConnections = new ArrayList<>();
3.5 PooledConnection
從POOLED的數(shù)據(jù)源中獲取到的連接都是Connection的動態(tài)代理實例,動態(tài)代理類就是PooledConnection,但與普通的動態(tài)代理不同的是每個Connection的動態(tài)代理實例都有一個唯一的動態(tài)代理類PooledConnection。
為什么要動態(tài)代理?
線程從POOLED中獲取到的連接后,可以對鏈接執(zhí)行任意的操作包括關(guān)閉鏈接的操作,但我們池化鏈接要是讓外部隨意就把鏈接關(guān)了,那么我們池中的鏈接可能慢慢的就都沒了,
所以不能讓連接被隨便的關(guān)閉,對于關(guān)閉的鏈接應(yīng)該放到空閑鏈接池中去給后續(xù)其它線程使用。
所以使用動態(tài)代理,PooledConnection代理類對于close方法并不會執(zhí)行colse而是將鏈接返回到空閑鏈接池中。
同時PooledConnection還記錄了連接的一些其它屬性:
- checkoutTimestamp:鏈接被從池中檢出的時間,也就是從POOLED數(shù)據(jù)源中被獲取到的時間,但檢測連接是否到達(dá)從獲取連接池檢出時間時就是比較的該時間。
- lastUsedTimestamp:鏈接最近被使用的時間,使用完后放回到空閑池的鏈接仍然是使用之前的lastUsedTimestamp時間。
- createdTimestamp:PooledConnection的創(chuàng)建時間
- realConnection:真正的鏈接
- proxyConnection:connection的動態(tài)代理實例。
- valid:鏈接是否還有效。
3.5.1 代理執(zhí)行
在創(chuàng)建PooledConnection是為其Connection創(chuàng)建一個動態(tài)代理實例,返回給外部的也正是這個動態(tài)代理的實例:
public PooledConnection(Connection connection, PooledDataSource dataSource) {
this.hashCode = connection.hashCode();
this.realConnection = connection;
this.dataSource = dataSource;
this.createdTimestamp = System.currentTimeMillis();
this.lastUsedTimestamp = System.currentTimeMillis();
this.valid = true;
// 動態(tài)代理實例
this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
}
在執(zhí)行時若是執(zhí)行的是close方法會建連接返回到連接池中,若是是Object的方法則直接執(zhí)行,若執(zhí)行的是非close和非Object中的方法,則會先驗證valid是否為true,也就是連接是否有效:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
// close方法將連接返回到空閑池
if (CLOSE.equals(methodName)) {
dataSource.pushConnection(this);
return null;
}
try {
// 非Object中繼承的發(fā)放先驗證vaild
if (!Object.class.equals(method.getDeclaringClass())) {
checkConnection();
}
return method.invoke(realConnection, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
private void checkConnection() throws SQLException {
if (!valid) {
throw new SQLException("Error accessing PooledConnection. Connection is invalid.");
}
}
3.5.2 判斷鏈接是否可用
在池中的鏈接會被一直持有者,也就是一直不會主動關(guān)閉,但難免會誤關(guān)閉或遇到一下特殊情況而“被”關(guān)閉導(dǎo)致的鏈接不可用的情況,例如mysql的鏈接在wait_timeout(默認(rèn)8小時)內(nèi)沒有與db發(fā)生交互那么mysql就會將鏈接置為過期,需要重新鏈接。所以判斷一個PooledConnection是真實可用的十分重要。
判斷PooledConnection是可用的首先valid必須是true并且realConnection不為null以及realConnection是可以‘ping’通的。
public boolean isValid() {
return valid && realConnection != null && dataSource.pingConnection(this);
}
3.6 PooledDataSource
POOLED數(shù)據(jù)源的連接池的DataSource,實現(xiàn)了DataSource接口。用于獲取和管理池化的連接。POOLED數(shù)據(jù)源的幾個配置最終就是影射到它的幾個字段上,并根據(jù)這幾個配置來管理池化連接。此外還有一個PoolState字段保存了連接池狀態(tài)。
public class PooledDataSource implements DataSource {
// 連接池狀態(tài)
private final PoolState state = new PoolState(this);
// 在任意時間可存在的活動(正在使用)連接數(shù)量
protected int poolMaximumActiveConnections = 10;
// 任意時間可能存在的空閑連接數(shù)
protected int poolMaximumIdleConnections = 5;
// 一個線程獲取連接時能容忍的連線獲取到壞連接數(shù)
protected int poolMaximumLocalBadConnectionTolerance = 3;
// 從active池中強制檢出的最大時間
protected int poolMaximumCheckoutTime = 20000;
// 沒有可用連接時再次嘗試獲取連接的等待時間
protected int poolTimeToWait = 20000;
// 是否進(jìn)行ping檢測
protected boolean poolPingEnabled;
// ping的頻率,默認(rèn)為0
protected int poolPingConnectionsNotUsedFor;
// ping檢測語句
protected String poolPingQuery = "NO PING QUERY SET";
}
每個SqlSessionFactory只有一個連接池即只會有一個PooledDataSource,但從中獲取連接的線程有多個,為了保證獲取連接是的線程安全,在PooledDataSource中獲取連接和將連接返回到空閑池時的臨界區(qū)使用synchronized對state加鎖來同步訪問。
// 獲取連接
private PooledConnection popConnection(String userName, String password) throws SQLException {
// ...
while (conn == null) {
synchronized (state) {
// ...
}
}
}
// 連接放回空閑池
protected void pushConnection(PooledConnection conn) throws SQLException {
synchronized (state) {
// ...
}
}
// 關(guān)閉所有連接
public void forceCloseAll() {
synchronized (state) {
}
}
3.6.1 pingConnection
PooledDataSource提供了ping檢測連接可用性。
pingConnection的ping探測不是我們常見的ping命令的探測,畢竟可ping通一個機(jī)器和這個機(jī)器的db鏈接可用可以說是是兩回事。
PooledDataSource的pingConnection先校驗連接是否已經(jīng)關(guān)閉了若已經(jīng)關(guān)閉了那么這個連接自然也就不能用了。
若連接未關(guān)閉并開啟了ping測試即poolPingEnabled配置為true,那么就使用連接執(zhí)行poolPingQuery指定的語句,語句執(zhí)行成功則任務(wù)鏈接正常,但若執(zhí)行時拋出任何異常都認(rèn)為鏈接不可用。
poolPingQuery的默認(rèn)配置為“NO PING QUERY SET”,這個語句是不符合sql語法的,執(zhí)行這個語句會拋出MySQLSyntaxErrorException異常,在執(zhí)行語句時拋出異常會認(rèn)為鏈接不可用,所以一定要記得配置poolPingQuery。
通過執(zhí)行語句來做檢測的‘成本’是很高的,所以配置poolPingQuery應(yīng)該盡量簡單并且沒有很大的查詢返回。同時pingConnection是ping檢測只有在連接從PooledDataSource中檢出的時間大于poolPingConnectionsNotUsedFor的時間是才進(jìn)行ping, poolPingConnectionsNotUsedFor默認(rèn)為0,建議將poolPingConnectionsNotUsedFor設(shè)置為和數(shù)據(jù)庫鏈接超時一樣避免不必要的檢測。
在進(jìn)行ping檢測是若檢測到鏈接不可用了,還會主動將鏈接close,當(dāng)再次但檢測這個鏈接時直接判斷closed而返回不用做sql執(zhí)行檢測。
protected boolean pingConnection(PooledConnection conn) {
boolean result = true;
// 已經(jīng)closed了那么必然是不可用了
try {
result = !conn.getRealConnection().isClosed();
} catch (SQLException e) {
return false;
}
// 啟用了ping且拼間隔時間大于等于0,同時返回使用的時間大于ping間隔時間則進(jìn)行檢測
if (result && poolPingEnabled
&& poolPingConnectionsNotUsedFor >= 0
&& conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor) {
try {
Connection realConn = conn.getRealConnection();
try (Statement statement = realConn.createStatement()) {
statement.executeQuery(poolPingQuery).close();
}
if (!realConn.getAutoCommit()) {
realConn.rollback();
}
result = true;
} catch (Exception e) {
try {
// 對不可用的連接主動關(guān)閉
conn.getRealConnection().close();
} catch (Exception e2) {
}
result = false;
}
}
return result;
}
什么時候進(jìn)行鏈接的有效性檢測?
只有以下兩個地方會進(jìn)行檢測:
- 從PooledDataSource獲取到鏈接時進(jìn)行檢測確保放回的鏈接是真的可用的。
private PooledConnection popConnection(String username, String password) throws SQLException {
// 獲取連接
...
// 獲取到連接對連接進(jìn)行檢測
if (conn != null) {
if (conn.isValid()) {
// ...
}
}
}
- 還有就是在連接用完放回到空閑鏈接池的時候進(jìn)行檢測確保放回的連接是可用的。
protected void pushConnection(PooledConnection conn) throws SQLException {
synchronized (state) {
// 從活躍連接池中移除
state.activeConnections.remove(conn);
// 連接是可用的則返回空閑鏈接池
if (conn.isValid()) {
// 返回空閑連接池
}
}
}
3.6.2 獲取鏈接
PooledDataSource實現(xiàn)了DataSource接口,其中的getConnection兩個方法通過調(diào)用popConnection來獲取連接:
@Override
public Connection getConnection() throws SQLException {
return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return popConnection(username, password).getProxyConnection();
}
popConnection獲取連接時經(jīng)過以下步驟:
1、若空閑連接池不為空則從空閑鏈接池只能夠獲取
if (!state.idleConnections.isEmpty()) {
conn = state.idleConnections.remove(0);
}
2、如果空閑連接池為空但活躍連接池未滿則新建一個連接
if (state.activeConnections.size() < poolMaximumActiveConnections) {
conn = new PooledConnection(dataSource.getConnection(), this);
} else
}
3、若活躍連接池已滿則嘗試從活躍連接中檢出第一個,若第一個連接已經(jīng)過期則將其從活躍連接池中移除并使用其realConnection新建一個PoolenConnection,若連接未設(shè)置自動提交則執(zhí)行以下回滾,避免新連接給到其它線程使用是誤提交了非自己的操作,將舊的連接置為無效,這樣若其它線程還使用就的連接進(jìn)行操作就會拋出異常。
PooledConnection oldestActiveConnection = state.activeConnections.get(0);
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
if (longestCheckoutTime > poolMaximumCheckoutTime) {
state.activeConnections.remove(oldestActiveConnection);
if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
try {
oldestActiveConnection.getRealConnection().rollback();
} catch (SQLException e) {
}
// 使用原來的連接建一個新的PooledConnection
conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
// 原來的連接置為無效
oldestActiveConnection.invalidate();
}
4、 活躍鏈接池中的第一個連接可能并沒有過期,那么就要等待poolTimeToWait時間后從新進(jìn)行第一步
try {
state.wait(poolTimeToWait);
} catch (InterruptedException e) {
break;
}
``
##### 3.5.7 釋放連接
先將連接從活躍連接池中移除。再校驗連接是否是有效的若無效了則不會添加到空閑池中。
若連有效且空閑連接池未滿,則使用被關(guān)閉的連接的realConnection新建一個連接并添加到空閑連接池中,并將原來置為無效,防止再被原來持有該連接的線程使用到。
若空閑連接池已滿則直接將連接關(guān)閉,當(dāng)然關(guān)閉前還是會對非自動提交的連接主動進(jìn)行回滾。
```java
protected void pushConnection(PooledConnection conn) throws SQLException {
synchronized (state) {
// 將連接從活躍連接中移除
state.activeConnections.remove(conn);
if (conn.isValid()) {
// 有效的連接才返回連接池
if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
state.accumulatedCheckoutTime += conn.getCheckoutTime();
if (!conn.getRealConnection().getAutoCommit()) {
// 非自動提交的主動回滾一下
conn.getRealConnection().rollback();
}
// 使用原來的連接新建一個
PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
state.idleConnections.add(newConn);
newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
// 原來的連接置為無效
conn.invalidate();
state.notifyAll();
}
}