Mybatis源碼分析之數據庫連接池DataSource
0、簡介
本篇文章主要記錄下學習Mybatis數據庫連接池的理解,本打算先寫解析mapper的源碼分析文章,隨後想想mapper等元素解析相關的文章單獨放一個系列記錄。此篇文章主要介紹Mybatis大的模塊分析。
此文章主要記錄以下幾個點:
1、爲什麼需要數據庫連接池
2、Mybatis數據庫連接池的分類
3、Mybatis數據庫連接池源碼分析
1、爲什麼需要數據庫連接池
一般提到池,大家第一印象就是池化後降低資源創建的消耗,獲取速度快等等。沒錯,就是需要達到這些效果。
首先演示下沒有池的情況下,每次查詢數據庫創建新的連接的耗時情況。
public class MainTest {
public static void main(String[] args) throws Exception {
Class.forName("com.mysql.jdbc.Driver");
Long beginTime = System.currentTimeMillis();
Connection connection = testOpenConn();
Long afterGetConn = System.currentTimeMillis();
// 我測試程序在testOpenConn()中獲取了10次連接,這裏將耗時除10
System.out.println("Get Conn consume time :"+(afterGetConn - beginTime)/10);
String statement = "select * from user";
PreparedStatement statement2 = connection.prepareStatement(statement);
statement2.executeQuery();
System.out.println("after execute consume time:"+(System.currentTimeMillis()-afterGetConn));
}
public static Connection testOpenConn() throws SQLException{
Connection connection = null;
for(int i=0;i<10;i++){
connection = (Connection) DriverManager.getConnection("jdbc:mysql://192.168.65.129:3306/test", "root", "root");
}
return connection;
}
}
程序在我電腦上執行的結果如下圖:
從上面的執行結果可以看出創建一個新的連接平均耗時 47 ms,那麼假設串行獲取10000個連接,光獲取連接耗時470000ms,7分多鐘,不敢想象呀。
下面我將Connection作爲成員變量緩存起來,然後執行10次查詢看看效果。
public class MainTest {
public static Connection connection;
static{
try {
Class.forName("com.mysql.jdbc.Driver");
connection = (Connection) DriverManager.getConnection("jdbc:mysql://192.168.65.129:3306/test", "root", "root");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
Long beginTime = System.currentTimeMillis();
for(int i=0;i<10;i++){
Connection connection = getConnection();
String statement = "select * from user";
PreparedStatement statement2 = connection.prepareStatement(statement);
statement2.executeQuery();
}
Long after = System.currentTimeMillis();
System.out.println("consume time :"+(after - beginTime)/10);
}
public static Connection getConnection() {
return connection;
}
}
執行結果如下圖:
不多說了,一切盡在結果中。由此可知,合理利用系統資源可以大大降低系統耗時,Mybatis的數據庫連接池的目的就是降低獲取資源耗時,提高資源利用率。
2、Mybatis數據庫連接池分類
首先看下配置文件中一般如何配置數據源的
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<property name="..." value="..."/>
</transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
再說下Mybatis什麼時候使用到數據庫連接?
來看下面一段熟悉的代碼
public class UserDaoTest {
@Test
public void findUsers(){
SqlSession sqlSession = getSessionFactory().openSession();
sqlSession.selectList("select * from user"); // 調用這一行代碼時候獲取數據庫連接
}
private static SqlSessionFactory getSessionFactory(){
SqlSessionFactory sqlSessionFactory = null;
String resource = "configuration.xml";
try {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream(resource));
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return sqlSessionFactory;
}
}
在上面代碼調用selectList()方法執行實際查詢時候會去獲取數據庫連接,我截取源碼中的部分代碼如下所示
// 其中會調用如下代碼獲取數據庫連接
Connection connection = getConnection(ms.getStatementLog());
// BaseExecutor中的代碼
protected Connection getConnection(Log statementLog) throws SQLException {
// 調用transaction.getConnection()獲取數據庫連接
Connection connection = transaction.getConnection();
if (statementLog.isDebugEnabled()) {
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
return connection;
}
}
// JdbcTransaction中實現的代碼
public Connection getConnection() throws SQLException {
if (connection == null) {
openConnection();
}
return connection;
}
// JdbcTransaction中實際調用dataSource.getConnection()方法獲取數據庫連接
protected void openConnection() throws SQLException {
if (log.isDebugEnabled()) {
log.debug("Opening JDBC Connection");
}
// 這段代碼到了實際的dataSource調用方法獲取數據庫連接對象了
connection = dataSource.getConnection();
if (level != null) {
connection.setTransactionIsolation(level.getLevel());
}
setDesiredAutoCommit(autoCommmit);
}
下面我們就要介紹到核心點了,在Mybatis項目源碼下我們可以看到有如下一個包,裏面就是Mybatis獲取數據源相關類
從圖中可以看出Mybatis的datasource包中主要有三個分類:① jndi ② pooled ③ unpooled。這三個包分別代表使用jndi獲取數據源相關、池化獲取數據源相關、非池化獲取數據源相關。
平常我們說的Mybaits的數據庫連接池一般都是和pooled包下 的類相關。下面我們就分別分析下pooled包和unpooled包下的相關類。
先看一張類的關係圖
2.1 UNPOOLED
unpooled包下的類如圖所示
下面先概要介紹下各個類的用處:
- UnpooledDataSource:普通的DataSource實現類,裏面實現了Mybatis相關的數據源邏輯。
- UnpooledDataSourceFactory : 非池化數據源工廠。
源碼的其他部分相對來說比較簡單,我們直接分析跟主題相關的代碼。下面就直接看UnpooledDataSource的getConnection()方法
// 下面就是UnpooledDataSource的getConnection()方法
public Connection getConnection() throws SQLException {
// 內部調用doGetConnection()方法
return doGetConnection(username, password);
}
private Connection doGetConnection(String username, String password) throws SQLException {
Properties props = new Properties();
if (driverProperties != null) {
props.putAll(driverProperties);
}
if (username != null) {
props.setProperty("user", username);
}
if (password != null) {
props.setProperty("password", password);
}
// 繼續調用doGetConnection()方法
return doGetConnection(props);
}
// 這段代碼想必大家非常熟悉吧
private Connection doGetConnection(Properties properties) throws SQLException {
// 加載驅動類
initializeDriver();
// 獲取數據庫連接
Connection connection = DriverManager.getConnection(url, properties);
// 配置連接(設置隔離級別、是否自動提交等)
configureConnection(connection);
return connection;
}
到這裏就基本上講完了UnpooledDataSource的主要功能了,和我們平常手動做實驗獲取數據庫連接基本差不多。
2.2 POOLED
pooled的包下的類如下圖所示,下面主要講下PooledDataSource相關源碼
下面先概要介紹下各個類的用處:
- PooledConnection : 對普通Connection的一個包裝,實現了InvocationHandler,方便後面通過代理來做一些操作。
- PooledDataSource : DataSource的實現類,組合了非池化數據源類。
- PooledDataSourceFactory : 數據源工廠類,繼承自非池化數據源工廠類。
- PooledState : 記錄數據庫連接池的一些狀態,比如空閒連接列表、活躍連接列表。
在分析實際源碼之前我先記錄下預備知識,從上面可以看出來pooled包下面比unpooled包明顯多出來兩個類:PooledConnection和PooledState
下面我們簡要介紹下相關類
① PooledConnection
介紹下PooledConnection類的要點和重要屬性
// 首先此類實現了InvacationHandler,不必多說,想要實現jdk動態代理
class PooledConnection implements InvocationHandler {
// 常量字符串,要對connection的close方法做特殊處理
private static final String CLOSE = "close";
// Connection接口類對象
private static final Class<?>[] IFACES = new Class<?>[] { Connection.class };
// 組合了PooledDataSource引用,方便一些操作
private final PooledDataSource dataSource;
// 真實數據庫連接對象
private final Connection realConnection;
// 代理數據庫連接對象
private final Connection proxyConnection;
// 構造函數
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;
// 直接使用此類生成一個Connection代理類
this.proxyConnection =(Connection)Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
}
// 代理方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
// 這裏可以看出,當調用PooledConnection生成的Connection的代理類的close方法時候
// 不是直接關閉連接,而是調用了dataSource.pushConnection(this)方法
// 這個方法我們後面分析
if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
dataSource.pushConnection(this);
return null;
}
try {
if (!Object.class.equals(method.getDeclaringClass())) {
// issue #579 toString() should never fail
// throw an SQLException instead of a Runtime
checkConnection();
}
return method.invoke(realConnection, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
② PoolState
PoolState類主要管理數據庫連接池的相關狀態,比如空閒列表、活躍列表等等。在這裏有個疑問就是這個類有點感覺沒必要,個人覺得完全可以將PoolState所有屬性和功能放入PooledDataSource類。後面再悟下。
public class PoolState {
// 有一個PooledDataSource的引用
protected PooledDataSource dataSource;
// 這個列表代表空閒連接列表
protected final List<PooledConnection> idleConnections = new ArrayList<>();
// 這個列表是活躍連接列表
protected final List<PooledConnection> activeConnections = new ArrayList<>();
// 構造方法
public PoolState(PooledDataSource dataSource) {
this.dataSource = dataSource;
}
}
③ PooledDataSource
public class PooledDataSource implements DataSource {
// 組合了一個PoolState對象
private final PoolState state = new PoolState(this);
// 組合了一個UnpooledDataSource對象,複用公共屬性和方法。
private final UnpooledDataSource dataSource;
// OPTIONAL CONFIGURATION FIELDS
// 下面是一些可以配置的屬性,見名知意
protected int poolMaximumActiveConnections = 10;
protected int poolMaximumIdleConnections = 5;
protected int poolMaximumCheckoutTime = 20000;
protected int poolTimeToWait = 20000;
protected int poolMaximumLocalBadConnectionTolerance = 3;
public PooledDataSource(String driver, String url, String username, String password) {
dataSource = new UnpooledDataSource(driver, url, username, password);
expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
}
}
好了,以上就是相關類的簡要介紹,下面我們進入正題,開始分析PooledDataSource的getConnection()方法,方法稍長,註釋一步一步解釋,先上一張流程圖,對照流程圖看代碼更清晰,流程圖中省略了中斷響應的步驟。
public Connection getConnection() throws SQLException {
// 內部調用popConnection方法
// dataSource是PooledDataSource內部組合的UnpooledDataSource類來存放數據庫用戶名等數據
return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
}
// 核心方法
private PooledConnection popConnection(String username, String password) throws SQLException {
boolean countedWait = false;
PooledConnection conn = null;
long t = System.currentTimeMillis();
int localBadConnectionCount = 0;
// 當conn爲null時候循環獲取conn
while (conn == null) {
// 對state加鎖
// state是PooledDataSource內部組合的PoolState對象
synchronized (state) {
// 如果空閒連接列表不爲空,那麼久直接從空閒列表裏面獲取一個連接
if (!state.idleConnections.isEmpty()) {
// Pool has available connection
conn = state.idleConnections.remove(0);
if (log.isDebugEnabled()) {
log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
}
} else {
// 若空閒列表爲空,但是活躍連接數量小於連接池配置的最大活躍連接數
// 那麼創建一個新的PooledConnection賦值給conn
if (state.activeConnections.size() < poolMaximumActiveConnections) {
// Can create new connection
conn = new PooledConnection(dataSource.getConnection(), this);
if (log.isDebugEnabled()) {
log.debug("Created connection " + conn.getRealHashCode() + ".");
}
} else {
// 空閒列表爲空,活躍連接數也不小於最大活躍數限制
// 那麼將活躍列表中最先加入的連接拿出來
PooledConnection oldestActiveConnection = state.activeConnections.get(0);
// 獲取其檢查時間(也可以理解爲加入活躍列表時候的時間)
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
// 如果檢查時間大於連接池最大檢查時間,那麼嘗試將其從活躍列表移除
if (longestCheckoutTime > poolMaximumCheckoutTime) {
// 變更狀態相關屬性
state.claimedOverdueConnectionCount++;
state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
state.accumulatedCheckoutTime += longestCheckoutTime;
// 從活躍連接列表移除加入列表時間超時的PooledConnection
state.activeConnections.remove(oldestActiveConnection);
// 被移除的連接的真實數據庫連接若不是設置了自動提交
// 那麼將其回滾
if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
try {
oldestActiveConnection.getRealConnection().rollback();
} catch (SQLException e) {
/*
Just log a message for debug and continue to execute the following
statement like nothing happened.
Wrap the bad connection with a new PooledConnection, this will help
to not interrupt current executing thread and give current thread a
chance to join the next competition for another valid/good database
connection. At the end of this loop, bad {@link @conn} will be set as null.
*/
log.debug("Bad connection. Could not roll back");
}
}
// 使用在活躍列表超時的連接的真實數據庫連接重新創建一個PooledConnection
// 將其賦值給conn
conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this); // 設置新連接的相關屬性
conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
// 置超時連接爲無效
oldestActiveConnection.invalidate();
if (log.isDebugEnabled()) {
log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
}
} else {
// 如果活躍列表中最先進入的連接的檢查時間都不大於最大檢查時間
// 那麼調用Object.wait(time)等待,等待期間可以響應中斷
// 等待結束後進行下一次while循環繼續獲取連接
try {
if (!countedWait) {
state.hadToWaitCount++;
countedWait = true;
}
if (log.isDebugEnabled()) {
log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
}
long wt = System.currentTimeMillis();
state.wait(poolTimeToWait);
state.accumulatedWaitTime += System.currentTimeMillis() - wt;
} catch (InterruptedException e) {
break;
}
}
}
}
// 如果上面代碼執行完以後conn不爲空,就說明拿到了一個連接
if (conn != null) {
// 判斷連接是否有效
if (conn.isValid()) {
// 如果PooledConnection的真實數據庫連接不是自動提交的
if (!conn.getRealConnection().getAutoCommit()) {
// 那麼使用之前必須先執行回滾操作
conn.getRealConnection().rollback();
}
conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
// 更新檢查時間爲當前時間
conn.setCheckoutTimestamp(System.currentTimeMillis());
conn.setLastUsedTimestamp(System.currentTimeMillis());
// 將連接加入活躍列表
state.activeConnections.add(conn);
state.requestCount++;
state.accumulatedRequestTime += System.currentTimeMillis() - t;
} else {
// 若連接是失效的
if (log.isDebugEnabled()) {
log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
}
// 將state的壞連接點計數+1
state.badConnectionCount++;
// 本地壞連接計數+1
localBadConnectionCount++;
// 將conn置空等待下次循環
conn = null;
// 如果本地壞連接數大於連接池最大空閒節點數和連接池最大本地壞連接容忍數之和
// 那麼就拋出異常
if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
if (log.isDebugEnabled()) {
log.debug("PooledDataSource: Could not get a good connection to the database.");
}
throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
}
}
}
}
}
if (conn == null) {
if (log.isDebugEnabled()) {
log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
}
throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
}
return conn;
}
最後講解下當PooledConnection連接使用完之後釋放相關的源碼分析,從上面PooledConnection類的簡要分析時候可知,PooledConnection內部組合了兩個Connection對象,一個是真實的數據庫連接對象,一個是Connection的代理對象。連接池對外使用的是Connection的代理對象,在代理內部除close()方法外的其他方法都會調用真實數據庫連接對象的相應方法method.invoke(realConnection, args)
,只有代理對象的close()方法調用時候邏輯如下
// 回想PooledConnection內部invoke方法中的判斷,當方法名是close的時候
if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
// 調用dataSource的pushConnection方法
// 此處的dataSource就是PooledDataSource對象
dataSource.pushConnection(this);
return null;
}
下面再來分析下pushConnection方法源碼,首先先看一張流程圖,對照流程圖看源碼邏輯更清晰。
protected void pushConnection(PooledConnection conn) throws SQLException {
// 先對state加鎖
synchronized (state) {
// 將conn從活躍列表中移除
state.activeConnections.remove(conn);
// 如果conn是有效的,那麼還要將conn本身"清除乾淨"
if (conn.isValid()) {
// 如果空閒列表小於最大空閒數量限制,且conn和數據庫連接池的其他連接是同型的
if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
// 將累計檢查時間+要放入空閒列表連接的檢查時間
state.accumulatedCheckoutTime += conn.getCheckoutTime();
// conn若是非自動提交,那麼放入空閒列表之前將conn中數據"清理"(回滾)
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
// 創建一個新的PooledConnection,將conn的真實數據庫連接放入
PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
// 將newConn放入空閒列表
state.idleConnections.add(newConn);
newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
// 將conn設置爲非法
conn.invalidate();
if (log.isDebugEnabled()) {
log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
}
// 喚醒在popConnection中wait的線程
state.notifyAll();
} else {
// 如果空閒列表滿了或者此連接和數據庫連接池中其他連接不同型
// 將state累積檢查時間加上conn的檢查時間
state.accumulatedCheckoutTime += conn.getCheckoutTime();
// 對conn進行"清理"
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
// 將conn的真實數據庫連接關閉
conn.getRealConnection().close();
if (log.isDebugEnabled()) {
log.debug("Closed connection " + conn.getRealHashCode() + ".");
}
// 設置conn非法標誌
conn.invalidate();
}
} else {
// 如果連接是無效的
if (log.isDebugEnabled()) {
log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
}
// 將state的壞連接計數+1
state.badConnectionCount++;
}
}
}
以上就是對本次看Mybatis數據庫連接池源碼的理解。若有不正確的地方,歡迎指正。