Mybatis數據源結構解析之鏈接池

對於 ORM 框架而言,數據源的組織是一個很是重要的一部分,這直接影響到框架的性能問題。本文將經過對 MyBatis 框架的數據源結構進行詳盡的分析,找出何時建立 Connection ,而且深刻解析 MyBatis 的鏈接池。java


本章的組織結構:

  • 零、什麼是鏈接池和線程池
  • 1、MyBatis 數據源 DataSource 分類
  • 2、數據源 DataSource 的建立過程
  • 3、 DataSource 何時建立 Connection 對象
  • 4、不使用鏈接池的 UnpooledDataSource
  • 5、使用了鏈接池的 PooledDataSource

鏈接池和線程池

鏈接池:(下降物理鏈接損耗)

  • 一、鏈接池是面向數據庫鏈接的
  • 二、鏈接池是爲了優化數據庫鏈接資源
  • 三、鏈接池有點相似在客戶端作優化

數據庫鏈接是一項有限的昂貴資源,一個數據庫鏈接對象均對應一個物理數據庫鏈接,每次操做都打開一個物理鏈接,使用完都關閉鏈接,這樣形成系統的性能低下。 數據庫鏈接池的解決方案是在應用程序啓動時創建足夠的數據庫鏈接,並將這些鏈接組成一個鏈接池,由應用程序動態地對池中的鏈接進行申請、使用和釋放。對於多於鏈接池中鏈接數的併發請求,應該在請求隊列中排隊等待。而且應用程序能夠根據池中鏈接的使用率,動態增長或減小池中的鏈接數。mysql


線程池:(下降線程建立銷燬損耗)

  • 一、線程池是面向後臺程序的
  • 二、線程池是是爲了提升內存和CPU效率
  • 三、線程池有點相似於在服務端作優化

線程池是一次性建立必定數量的線程(應該能夠配置初始線程數量的),當用請求過來不用去建立新的線程,直接使用已建立的線程,使用後又放回到線程池中。
避免了頻繁建立線程,及銷燬線程的系統開銷,提升是內存和CPU效率。sql

相同點:

都是事先準備好資源,避免頻繁建立和銷燬的代價。數據庫

數據源的分類

在Mybatis體系中,分爲3DataSource緩存

打開Mybatis源碼找到datasource包,能夠看到3個子packagesession

  • UNPOOLED 不使用鏈接池的數據源mybatis

  • POOLED 使用鏈接池的數據源併發

  • JNDI 使用JNDI實現的數據源app

MyBatis內部分別定義了實現了java.sql.DataSource接口的UnpooledDataSource,PooledDataSource類來表示UNPOOLED、POOLED類型的數據源。 以下圖所示:框架

  • PooledDataSource和UnpooledDataSrouce都實現了java.sql.DataSource接口.
  • PooledDataSource持有一個UnPooledDataSource的引用,當PooledDataSource要建立Connection實例時,實際仍是經過UnPooledDataSource來建立的.(PooledDataSource)只是提供一種緩存鏈接池機制.

JNDI類型的數據源DataSource,則是經過JNDI上下文中取值。

數據源 DataSource 的建立過程

在mybatis的XML配置文件中,使用 元素來配置數據源:

<!-- 配置數據源(鏈接池) -->
<dataSource type="POOLED"> //這裏 type 屬性的取值就是爲POOLED、UNPOOLED、JNDI
  <property name="driver" value="${jdbc.driver}"/>
  <property name="url" value="${jdbc.url}"/>
  <property name="username" value="${jdbc.username}"/>
  <property name="password" value="${jdbc.password}"/>
</dataSource>

MyBatis在初始化時,解析此文件,根據<dataSource>的type屬性來建立相應類型的的數據源DataSource,即:

  • type=」POOLED」 :建立PooledDataSource實例

  • type=」UNPOOLED」 :建立UnpooledDataSource實例

  • type=」JNDI」 :從JNDI服務上查找DataSource實例


    Mybatis是經過工廠模式來建立數據源對象的 咱們來看看源碼:

    public interface DataSourceFactory {
    
    void setProperties(Properties props);
    
    DataSource getDataSource();//生產DataSource
    
    }

    上述3種類型的數據源,對應有本身的工廠模式,都實現了這個DataSourceFactory

MyBatis建立了DataSource實例後,會將其放到Configuration對象內的Environment對象中, 供之後使用。

注意dataSource 此時只會保存好配置信息.鏈接池此時並無建立好鏈接.只有當程序在調用操做數據庫的方法時,纔會初始化鏈接.

DataSource何時建立Connection對象

咱們須要建立SqlSession對象並須要執行SQL語句時,這時候MyBatis纔會去調用dataSource對象來建立java.sql.Connection對象。也就是說,java.sql.Connection對象的建立一直延遲到執行SQL語句的時候。

例子:

@Test
  public void testMyBatisBuild() throws IOException {
      Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
      SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
      SqlSession sqlSession = factory.openSession();
      TestMapper mapper = sqlSession.getMapper(TestMapper.class);
      Ttest one = mapper.getOne(1L);//直到這一行,纔會去建立一個數據庫鏈接!
      System.out.println(one);
      sqlSession.close();
  }

口說無憑,跟進源碼看看他們是在何時建立的...

跟進源碼,驗證Datasource 和Connection對象建立時機

驗證Datasource建立時機
  • 上面咱們已經知道,pooled數據源實際上也是使用的unpooled的實例,那麼咱們在UnpooledDataSourceFactory的
    getDataSource方法的源碼中作一些修改 並運行測試用例:
@Override
public DataSource getDataSource() {//此方法是UnpooledDataSourceFactory實現DataSourceFactory複寫
  System.out.println("建立了數據源");
  System.out.println(dataSource.toString());
  return dataSource;
}

結論:在建立完SqlsessionFactory時,DataSource實例就建立了.


驗證Connection建立時機

首先咱們先查出如今數據庫的全部鏈接數,在數據庫中執行

SELECT * FROM performance_schema.hosts;

返回數據: 顯示當前鏈接數爲1,總鏈接數70

show full processlist; //顯示全部的任務列表

返回:當前只有一個查詢的鏈接在運行

從新啓動項目,在運行到須要執行實際的sql操做時,能夠看到他已經被代理加強了

直到此時,鏈接數仍是沒有變,說明鏈接尚未建立,咱們接着往下看.

咱們按F7進入方法,能夠看到,他被代理,,這時候會執行到以前的代理方法中調用invoke方法.這裏有一個判斷,可是並不成立,因而進入cachedInvoker(method).invoke()方法代理執行一下操做

cachedInvoker(method).invoke()方法

@Override
    public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
      return mapperMethod.execute(sqlSession, args);
    }

繼續F7進入方法,因爲咱們是單條查詢select 因此會case進入select塊中的selectOne

繼續F7

繼續F7

經過configuration.getMappedStatement獲取MappedStatement

單步步過,F8後,進入executor.query方法

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  BoundSql boundSql = ms.getBoundSql(parameterObject);
  CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
  return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

繼續走到底,F7進入query方法

此時,會去緩存中查詢,這裏的緩存是二級緩存對象 ,生命週期是mapper級別的(一級緩存是一個session級別的),由於咱們此時是第一次運行程序,因此確定爲Null,這時候會直接去查詢,調用delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql)方法,F7進入這個方法

二級緩存沒有獲取到,又去查詢了一級緩存,發現一級緩存也沒有,這個時候,纔去查數據庫

queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);//沒有緩存則去db查

F7進入queryFromDatabase方法.看到是一些對一級緩存的操做,咱們主要看doQuery方法F7進入它.

能夠看到它準備了一個空的Statement

咱們F7跟進看一下prepareStatement方法 ,發現他調用了getConnection,哎!有點眼熟了,繼續F7進入getConnection()方法,

又是一個getConnection()....繼續F7進入transaction.getConnection()方法

又是一個getConnection()方法.判斷connection是否爲空.爲空openConnection()不然直接返回connection;咱們F7繼續跟進openConnection()方法

protected void openConnection() throws SQLException {
  if (log.isDebugEnabled()) {
    log.debug("Opening JDBC Connection");
  }
  connection = dataSource.getConnection();//最終獲取鏈接的地方在這句.
  if (level != null) {
    connection.setTransactionIsolation(level.getLevel());//設置隔離等級
  }
  setDesiredAutoCommit(autoCommit);//是否自動提交,默認false,update不會提交到數據庫,須要手動commit
}

dataSource.getConnection()執行完,至此一個connection才建立完成.
咱們驗證一下 在dataSource.getConnection()時打一下斷點.

此時數據庫中的鏈接數依然沒變 仍是1

咱們按F8 執行一步

在控制檯能夠看到connection = com.mysql.jdbc.JDBC4Connection@1500b2f3 實例建立完畢 咱們再去數據庫中看看鏈接數

兩個鏈接分別是:

不使用鏈接池的 UnpooledDataSource

的type屬性被配置成了」UNPOOLED」,MyBatis首先會實例化一個UnpooledDataSourceFactory工廠實例,而後經過.getDataSource()方法返回一個UnpooledDataSource實例對象引用,咱們假定爲dataSource。
使用UnpooledDataSource的getConnection(),每調用一次就會產生一個新的Connection實例對象。

UnPooledDataSource的getConnection()方法實現以下:

/*
UnpooledDataSource的getConnection()實現
*/
public Connection getConnection() throws SQLException
{
  return doGetConnection(username, password);
}

private Connection doGetConnection(String username, String password) throws SQLException
{
  //封裝username和password成properties
  Properties props = new Properties();
  if (driverProperties != null)
  {
      props.putAll(driverProperties);
  }
  if (username != null)
  {
      props.setProperty("user", username);
  }
  if (password != null)
  {
      props.setProperty("password", password);
  }
  return doGetConnection(props);
}

/*
*  獲取數據鏈接
*/
private Connection doGetConnection(Properties properties) throws SQLException
{
  //1.初始化驅動
  initializeDriver();
  //2.從DriverManager中獲取鏈接,獲取新的Connection對象
  Connection connection = DriverManager.getConnection(url, properties);
  //3.配置connection屬性
  configureConnection(connection);
  return connection;
}

UnpooledDataSource會作如下幾件事情:

    1. 初始化驅動: 判斷driver驅動是否已經加載到內存中,若是尚未加載,則會動態地加載driver類,並實例化一個Driver對象,使用DriverManager.registerDriver()方法將其註冊到內存中,以供後續使用。
    1. 建立Connection對象: 使用DriverManager.getConnection()方法建立鏈接。
    1. 配置Connection對象: 設置是否自動提交autoCommit和隔離級別isolationLevel。
    1. 返回Connection對象。

咱們每調用一次getConnection()方法,都會經過DriverManager.getConnection()返回新的java.sql.Connection實例,這樣固然對於資源是一種浪費,爲了防止重複的去建立和銷燬鏈接,因而引入了鏈接池的概念.

使用了鏈接池的 PooledDataSource

要理解鏈接池,首先要了解它對於connection的容器,它使用PoolState容器來管理全部的conncetion


在PoolState中,它將connection分爲兩種狀態,空閒狀態(idle)活動狀態(active),他們分別被存儲到PoolState容器內的idleConnectionsactiveConnections兩個ArrayList中

  • idleConnections:空閒(idle)狀態PooledConnection對象被放置到此集合中,表示當前閒置的沒有被使用的PooledConnection集合,調用PooledDataSource的getConnection()方法時,會優先今後集合中取PooledConnection對象。當用完一個java.sql.Connection對象時,MyBatis會將其包裹成PooledConnection對象放到此集合中。

  • activeConnections:活動(active)狀態的PooledConnection對象被放置到名爲activeConnections的ArrayList中,表示當前正在被使用的PooledConnection集合,調用PooledDataSource的getConnection()方法時,會優先從idleConnections集合中取PooledConnection對象,若是沒有,則看此集合是否已滿,若是未滿,PooledDataSource會建立出一個PooledConnection,添加到此集合中,並返回。

從鏈接池中獲取一個鏈接對象的過程

下面讓咱們看一下PooledDataSource 的popConnection方法獲取Connection對象的實現:

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) {//給state對象加鎖
        if (!state.idleConnections.isEmpty()) {//若是空閒列表不空,就從空閒列表中拿connection
          // Pool has available connection
          conn = state.idleConnections.remove(0);//拿出空閒列表中的第一個,去驗證鏈接是否還有效
          if (log.isDebugEnabled()) {
            log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
          }
        } else {
          // 空閒鏈接池中沒有可用的鏈接,就來看看活躍鏈接列表中是否有..先判斷活動鏈接總數 是否小於 最大可用的活動鏈接數
          if (state.activeConnections.size() < poolMaximumActiveConnections) {
            // 若是鏈接數小於list.size 直接建立新的鏈接.
            conn = new PooledConnection(dataSource.getConnection(), this);
            if (log.isDebugEnabled()) {
              log.debug("Created connection " + conn.getRealHashCode() + ".");
            }
          } else {
            // 此時鏈接數也滿了,不能建立新的鏈接. 找到最老的那個,檢查它是否過時
            //計算它的校驗時間,若是校驗時間大於鏈接池規定的最大校驗時間,則認爲它已通過期了
            // 利用這個PoolConnection內部的realConnection從新生成一個PooledConnection
            PooledConnection oldestActiveConnection = state.activeConnections.get(0);
            long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
            if (longestCheckoutTime > poolMaximumCheckoutTime) {
              // 能夠要求過時這個鏈接.
              state.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 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");
                }
              }
              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 {
              //若是不能釋放,則必須等待
              // Must wait
              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;
              }
            }
          }
        }
        if (conn != null) {
          // ping to server and check the connection is valid or not
          if (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;
  }

如上所示,對於PooledDataSource的getConnection()方法內,先是調用類PooledDataSource的popConnection()方法返回了一個PooledConnection對象,而後調用了PooledConnection的getProxyConnection()來返回Connection對象。

複用鏈接的過程

若是咱們使用了鏈接池,咱們在用完了Connection對象時,須要將它放在鏈接池中,該怎樣作呢? 若是讓咱們來想的話,應該是經過代理Connection對象,在調用close時,並不真正關閉,而是丟到管理鏈接的容器中去. 要驗證這個想法 那麼 來看看Mybatis幫咱們怎麼實現複用鏈接的.

class PooledConnection implements InvocationHandler {

  //......
  //所建立它的datasource引用
  private PooledDataSource dataSource;
  //真正的Connection對象
  private Connection realConnection;
  //代理本身的代理Connection
  private Connection proxyConnection;

  //......
}

PooledConenction實現了InvocationHandler接口,而且,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;
    this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
  }

實際上,咱們調用PooledDataSource的getConnection()方法返回的就是這個proxyConnection對象。
當咱們調用此proxyConnection對象上的任何方法時,都會調用PooledConnection對象內invoke()方法。
讓咱們看一下PooledConnection類中的invoke()方法定義:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    String methodName = method.getName();
    //當調用關閉的時候,回收此Connection到PooledDataSource中
    if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
      dataSource.pushConnection(this);
      return null;
    } else {
      try {
        if (!Object.class.equals(method.getDeclaringClass())) {
          checkConnection();
        }
        return method.invoke(realConnection, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
  }

結論:當咱們使用了pooledDataSource.getConnection()返回的Connection對象的close()方法時,不會調用真正Connection的close()方法,而是將此Connection對象放到鏈接池中。調用dataSource.pushConnection(this)實現

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();
          if (log.isDebugEnabled()) {
            log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
          }
          state.notifyAll();
        } else {
          state.accumulatedCheckoutTime += conn.getCheckoutTime();
          if (!conn.getRealConnection().getAutoCommit()) {
            conn.getRealConnection().rollback();
          }
          conn.getRealConnection().close();
          if (log.isDebugEnabled()) {
            log.debug("Closed connection " + conn.getRealHashCode() + ".");
          }
          conn.invalidate();
        }
      } else {
        if (log.isDebugEnabled()) {
          log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
        }
        state.badConnectionCount++;
      }
    }
  }

關注公衆號:java寶典
a

相關文章
相關標籤/搜索