《深入理解Mybatis原理》 02-Mybatis数据源与连接池
??? 對于ORM框架而言,數據源的組織是一個非常重要的一部分,這直接影響到框架的性能問題。本文將通過對MyBatis框架的數據源結構進行詳盡的分析,并且深入解析MyBatis的連接池。
??? 本文首先會講述MyBatis的數據源的分類,然后會介紹數據源是如何加載和使用的。緊接著將分類介紹UNPOOLED、POOLED和JNDI類型的數據源組織;期間我們會重點講解POOLED類型的數據源和其實現的連接池原理。
?
本文結構如下:
- ??????? 一、MyBatis數據源DataSource分類
- ??????? 二、數據源DataSource的創建過程
- ??????? 三、 DataSource什么時候創建Connection對象
- ??????? 四、不使用連接池的UnpooledDataSource
- ??????? 五、為什么要使用連接池?
- ??????? 六、使用了連接池的PooledDataSource
?
一、Mybatis數據源分類
?? mybatis數據源實現類在mybatis的dataSource包中:
?? Mybatis將數據源分為三種:
?? JNDI 數據源 : 使用JNDI方式數據源
?? POOLED數據眼:? 使用連接池數據源
?? UNPOOLED 數據源 : 不使用連接池數據源
即:
?
相應的Mybatis內部分別以實現? javax.sql.DataSource 接口的 PooledDataSource和 UnPooledDataSource 實現 POOLED和UNPOOLED數據源。(關于數據源創建細節請看下面章節)
JNDI數據源則通過 javax.naming.Context 上下文生成數據源。
?
二、數據源DataSource創建過程
?數據源配置如下:
<dataSource type="UNPOOLED"><property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/pmdb"/><property name="username" value="root"/><property name="password" value="123456"/></dataSource>? Mybatis數據源的創建過程:
? Mybatis初始化階段 就會創建好數據源,具體創建數據源的時機發生在解析mybatis XML配置文件<environments></environments>? 節點下的<dataSource></dataSouorce>節點:
//context :dataSource節點數據private DataSourceFactory dataSourceElement(XNode context) throws Exception {if (context != null) {//獲取dataSource配置的類型 (POOLED、UNPOOLED、JNDI)String type = context.getStringAttribute("type");//將dataSource下的username、password等信息解析為PropertiesProperties props = context.getChildrenAsProperties();//根據dataSource的type類型(別名機制)獲取到對應的DateSource實現類,并實例化該類DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();factory.setProperties(props);return factory;}throw new BuilderException("Environment declaration requires a DataSourceFactory.");}? 創建DataSource最關鍵的一步在:? DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
? 打開該方法resolveClass方法實現,看它到底做了什么:
public <T> Class<T> resolveAlias(String string) {try {if (string == null) {return null;}String key = string.toLowerCase(Locale.ENGLISH);Class<T> value;if (TYPE_ALIASES.containsKey(key)) {value = (Class<T>) TYPE_ALIASES.get(key);} else {value = (Class<T>) Resources.classForName(string);}return value;} catch (ClassNotFoundException e) {throw new TypeException("Could not resolve type alias '" + string + "'. Cause: " + e, e);}}???? resolveClass方法核心功能就是根據XML dataSource節點配置的type屬性找到對應的實現類:
??? 如上圖所示:
???? 根據配置的type別名找到Factory,然后創建出對應的DataSource
- JNDI : JndiDataSourceFactory
- POOLED: PooledDataSourceFactory
- UNPOOLED: UnpooledDataSourceFactory
?
Mybatis創建DataSource之后會將其放在Configuration的Environment中,供以后使用。?
三、DataSource什么時候創建Connection對象
InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);SqlSession sqlSession = sqlSessionFactory.openSession();User user = (User) sqlSession.selectOne("selectByPrimaryKey", 1);??? 如上圖所示,前三行代碼都不會去創建javax.sql.Connection,當執行到 selectOne("selectByPrimaryKey", 1);時,才會去真正創建Connection對象:
@Overridepublic <E> List<E> doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)throws SQLException {Statement stmt = null;try {flushStatements();Configuration configuration = ms.getConfiguration();StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject, rowBounds, resultHandler, boundSql);Connection connection = getConnection(ms.getStatementLog());stmt = handler.prepare(connection, transaction.getTimeout());handler.parameterize(stmt);return handler.<E>query(stmt, resultHandler);} finally {closeStatement(stmt);}}?那對于UNPOOLED類型DataSource的實現UnpooledDataSource是怎么樣實現getConnection方法呢?請看一下節。
?
四、不使用連接池的UnpooledDataSource
//UnpoolDataSource 創建Connection對象 private Connection doGetConnection(Properties properties) throws SQLException {//1. 初始化驅動initializeDriver();//2. 創建Connection對象Connection connection = DriverManager.getConnection(url, properties);//3. 配置ConnectionconfigureConnection(connection);return connection;}? 如上代碼所示,流程如下:
- ? 1. 初始化驅動 : 判斷驅動是否加載到內存中,若有則直接取出,否則創建驅動
- ? 2. 創建Connecion : 調用DriverManager創建相應的Connection對象
- ? 3. 配置Connection: 配置Connection對象一些默認配置項
- ? 4. 返回DataSource對象: 返回創建好的Connection對象供以使用
? 總結:從上述的代碼中可以看到,我們每調用一次getConnection()方法,都會通過DriverManager.getConnection()返回新的java.sql.Connection實例。
?
五、為什么要使用連接池?
???
public static void main(String[] args) throws IOException, ClassNotFoundException, SQLException {long start = System.currentTimeMillis();Class.forName("com.mysql.jdbc.Driver");Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mall", "root", "123456");System.out.println("創建Connection對象耗時 : " + String.valueOf(System.currentTimeMillis() - start));String sql = "select * from mmall_user where id = 1";start = System.currentTimeMillis();Statement statement = connection.createStatement();ResultSet resultSet = statement.executeQuery(sql);System.out.println("查詢語句耗時:" + String.valueOf(System.currentTimeMillis() - start));while(resultSet.next()){System.out.println("id = " + resultSet.getInt(1) + " username = " + resultSet.getString(2));}resultSet.close();statement.close();connection.close();}????
創建Connection對象耗時 789ms, 而查詢語句才耗時7ms.(不排除數據庫數據少的原因,但是查詢耗時一般不會超過789ms)
一次查詢請求創建Connection對象耗時789ms。要知道100ms對于Java來說都是很奢侈的。(一個Connection對象耗時 700ms,10000 * 700 =? 116分鐘,10000次請求只創建對象就耗時116分鐘,這是根本不能接受的)
所以使用連接池是非常有必要的。
?
六、使用了連接池的PooledDataSource
?? 了解連接池之前,先了解兩個參數概念:
?? idleConnections : 空閑Connection對象,當其他請求需要創建Connection時,直接到ideaConnection取出一個連接,可以減少資源、耗時。
?? activeConnections : 活動Connection對象,記錄當前正在被請求所使用的Connection對象,當一次請求使用完一個Connection時,不將其立即銷毀,而是放到idleConnection緩存池里面。
?
??
? 對于UnpooledDataSource每次請求都會創建一個新的Connection對象,當請求結束后會執行Connection.cloes()方法關閉該Connection.
???? PooledDataSource是如何創建的Connection的呢?
@Overridepublic Connection getConnection() throws SQLException {return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();}@Overridepublic Connection getConnection(String username, String password) throws SQLException {return popConnection(username, password).getProxyConnection();}? 數據源type設置為 POOLED,當實例化DataSource時會根據別名實例化出 PooledDataSource對象。
? 當調用getConnection方法創建Connection時,最終會調用 popConnecion方法并返回一個代理對象。
?
private PooledConnection popConnection(String username, String password) throws SQLException {boolean countedWait = false;PooledConnection conn = null;long t = System.currentTimeMillis();int localBadConnectionCount = 0;while (conn == null) {synchronized (state) {//判斷連接池中是否還有空閑Connection對象,若有則直接返回一個Connectionif (!state.idleConnections.isEmpty()) {// Pool has available connectionconn = state.idleConnections.remove(0);if (log.isDebugEnabled()) {log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");}} else {//沒有空閑Connection對象//當前活動對象個數是小于最大活動數量 則會生成一個新的Connection對象if (state.activeConnections.size() < poolMaximumActiveConnections) {// Can create new connectionconn = new PooledConnection(dataSource.getConnection(), this);if (log.isDebugEnabled()) {log.debug("Created connection " + conn.getRealHashCode() + ".");}} else {//判斷老的活動對象是否超過poolMaximumCheckoutTime時間PooledConnection oldestActiveConnection = state.activeConnections.get(0);long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();//超過poolMaximumCheckoutTime時間,則嘗試結束該Connection對象線程,并返回重用Connectionif (longestCheckoutTime > poolMaximumCheckoutTime) {// Can claim overdue connectionstate.claimedOverdueConnectionCount++;state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;state.accumulatedCheckoutTime += longestCheckoutTime;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 followingstatement like nothing happend.Wrap the bad connection with a new PooledConnection, this will helpto not intterupt current executing thread and give current thread achance to join the next competion for another valid/good databaseconnection. At the end of this loop, bad {@link @conn} will be set as null.*/log.debug("Bad connection. Could not roll back");} }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 {//沒有超時,則等待該Connection線程結束// Must waittry {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;}}}}if (conn != null) {// ping to server and check the connection is valid or notif (conn.isValid()) {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.badConnectionCount++;localBadConnectionCount++;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;}綜上所述,大致流程如下:
- 1. 若IdleConnection中是否有空閑的連接對象,則直接返回Connection
- 2. 判斷當前activeConnection數量是否小于poolMaximumActiveConnections(活動連接最大數量),若小于,則和Unpooled方式一樣創建新的Connection對象并返回。若大于,則會判斷當前所有活動連接的占用時間是否超時,若超時則停止該Connection,并直接返回供其他請求使用。若沒有超時,則等待Connection使用完畢后再返回。
?
? 連接池對Connection.close()的處理:
PooledDataSource中除了popConnection方法,還有一個pushConnection方法pushConnection方法會將使用完畢的Connection放入idleConnections緩存池中,供其他請求繼續使用。傳統的jdbc連接使用完Connection之后,會手動執行Connection.cloes()方法關閉連接。Pooled連接池為了重復利用Connection減少不必要的開銷,對Connection.cloes做了動態代理。也就是說,在Pooled模式下,若我們手動執行connecion.cloes(),實際上并不會執行原生Connection.close方法。而是通過PooledConnection對原生Connection做動態代理,把close方法映射到 pushConnection方法上:@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {String methodName = method.getName();//若執行close方法,實際上會代理執行pushConnection方法if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {dataSource.pushConnection(this);return null;} else {try {if (!Object.class.equals(method.getDeclaringClass())) {// issue #579 toString() should never fail// throw an SQLException instead of a RuntimecheckConnection();}return method.invoke(realConnection, args);} catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);}}}?
以上就是本文 《深入理解Mybatis原理》 02-Mybatis數據源與連接池?的全部內容,
上述內容如有不妥之處,還請讀者指出,共同探討,共同進步!
@author : jackcheng1117@163.com
總結
以上是生活随笔為你收集整理的《深入理解Mybatis原理》 02-Mybatis数据源与连接池的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: TF卡 SD卡接口【转】
- 下一篇: 新年新的想法(一直放在草稿箱里到现在)