1. 程式人生 > >Mybatis 原始碼分析:資料來源與連線池

Mybatis 原始碼分析:資料來源與連線池

1. mybatis 資料來源原理分析

mybatis 資料來源 DataSource 的建立是在解析配置檔案 <environment /> 元素下子元素 <dataSource /> 時建立的。配置如下:

<dataSource type="POOLED">
	<property name="url" value="" />
	<property name="driver" value="" />
	<property name="user" value="" />
	<property name="password" value
="" />
</dataSource>

可以看到 <dataSource /> 元素有個屬性為 type,它表示你想要建立什麼樣型別的資料來源,mybatis 提供三種類型:POOLED、UNPOOLED 和 JNDI,mybatis 自身提供了對 javax.sql.DataSource 的兩種實現:PooledDataSource 和 UnpooledDataSource,而 PooledDataSource 持有對 UnpooledDataSource 的引用,它們類圖如下: DataSource DataSource 是有了,那麼 mybatis 是在什麼時候獲取 Connection 物件的呢?PooledDataSource 和 UnpooledDataSource 在獲取 Connection 時又有什麼區別?

2. Connection 獲取原理分析

我們都知道獲取 Connection 物件肯定是在要執行 sql 語句之前獲取到的,即獲取 PreparedStatement 物件時。因此我們看看 SimpleExecutor 的 prepareStatement() 方法做了什麼事情。

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection
(statementLog); stmt = handler.prepare(connection, transaction.getTimeout()); handler.parameterize(stmt); return stmt; }

可以看到其中有一行程式碼呼叫了 getConnection() 方法,可以不用管傳遞的引數是什麼,不影響分析獲取 Connection 物件流程。

protected Connection getConnection(Log statementLog) throws SQLException {
    Connection connection = transaction.getConnection();
    if (statementLog.isDebugEnabled()) {
      return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    } else {
      return connection;
    }
}

在這裡可以看到它又去呼叫了 Transaction(抽象事物物件) 介面的 getConnection() 方法,在上一篇文章有說過 mybatis 的事物管理機制,在這裡就不多說了。我們就以 JdbcTransaction 實現來講解這個方法做了啥。

@Override
public Connection getConnection() throws SQLException {
	if (connection == null) {
	  openConnection();
	}
	return connection;
}

終於看到了獲取 Connection 物件的曙光了,這裡又呼叫了 openConnection() 方法:

protected void openConnection() throws SQLException {
    connection = dataSource.getConnection();
    if (level != null) {
      connection.setTransactionIsolation(level.getLevel());
    }
    setDesiredAutoCommit(autoCommmit);
}

此時,真正從資料來源獲取 Connection 物件了,在瞭解 mybatis 怎麼實現快取資料庫連線之前,我們先從簡單的開始瞭解,瞭解下通過 mybatis 提供的 UnpooledDataSource 獲取 Connection 的過程。然後再說 PooledDataSource 獲取 Connection 的過程。

3. UnpooledDataSource 獲取 Connection 原理分析

@Override
public Connection getConnection() throws SQLException {
    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);
    }
    
    return doGetConnection(props);
}
private Connection doGetConnection(Properties properties) throws SQLException {
	// 載入驅動
    initializeDriver();
    // 從 DriverManager 獲取 Connection 物件 
    Connection connection = DriverManager.getConnection(url, properties);
    // 配置是否自動提交和事物隔離級別
    configureConnection(connection);
    return connection;
}

可以看到,doGetConnection() 方法 主要做三件事:

  • 初始化 Driver:

    判斷 Driver 驅動是否已經載入到記憶體中,如果還沒有載入,則會動態地載入 Driver 類,並例項化一個 Driver 物件,使用 DriverManager.registerDriver() 方法將其註冊到記憶體中,以供後續使用,並快取。

  • 建立 Connection 物件:

    使用 DriverManager.getConnection() 方法建立連線。

  • 配置 Connection 物件:

    設定是否自動提交和隔離級別。

它的時序圖如下所示: 時序圖

那麼問題來了,為什麼需要對資料庫連線 Connection 進行池化呢?我們可以想象一下,應用程式連線資料庫,底層肯定用的是 TCP 網路通訊,我們都知道,建立 TCP 連線需要進行三次握手,執行完一條 SQL 語句就釋放 TCP 連線(釋放需要四次握手),那麼這是一個比較耗時耗資源的操作,如果當對資料庫請求量比較大的話,那麼每一次請求就建立一次 TCP 連線,這使用者肯定受不了響應時間。我們也可以簡單測試一下獲取 Connection 物件一般需要多長時間:

long start = System.currentTimeMillis();
Connectionn conn =DriverManager.getConnection("url");
long end = System.currentTimeMillis();
System.out.println("consumed time is " + (end - start));

所以,我們需要對 Connection 物件進行池化,而 mybatis 剛好也為我們提供了池化的資料來源:PooledDataSource,那我們就來看看它是怎麼對 Connection 進行池化的。

4. PooledDataSource 獲取 Connection 原理分析

先不管三七二十一,檢視下 PooledDataSource 下的 getConnection() 方法做了什麼:

@Override
public Connection getConnection() throws SQLException {
    return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
}

可以看到它去呼叫了 popConnection() 方法,從方法名翻譯過來就是“彈出連線”,那麼這個方法肯定會從集合裡獲取連線,我們來看看是否如猜測一樣:

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) {
      	// 是否有空閒連線
        if (!state.idleConnections.isEmpty()) {
          // 取出第一個空閒連線
          conn = state.idleConnections.remove(0);
          if (log.isDebugEnabled()) {
            log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
          }
        } else {
          // 活躍連線數是否達到最大限定數
          if (state.activeConnections.size() < poolMaximumActiveConnections) {
            // 建立新連線
            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) {
              // Can claim overdue connection
              state.claimedOverdueConnectionCount++;
              state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
              state.accumulatedCheckoutTime += longestCheckoutTime;
              state.activeConnections.remove(oldestActiveConnection);
              if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
                try {
                  oldestActiveConnection.getRealConnection().rollback();
                } catch (SQLException e) {
                  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 {
              // 如果不能釋放,必須等待
              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;
              }
            }
          }
        }
        // 獲取 PooledConnection 成功,更新資訊
        if (conn != null) {
          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 + 3)) {
              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() 方法返回的是 javax.sql.Connection 型別,而 popConnection() 方法返回的是 mybatis 本身提供的 PooledConnection,而看 PooledConnection 繼承體系,發現它並沒有實現 Connection 介面,而是實現了 java.lang.reflect.InvocationHandler 介面,這個介面是為 jdk 動態代理功能提供的,確實也在呼叫 popConnection() 方法之後,呼叫了 PooledConnection 類的 getProxyConnection() 方法,獲取 Connection 物件的代理例項。那麼問題來了,為什麼 mybatis 在這裡要提供一個 Connection 物件的代理呢?

個人覺得有如下原因:

保持正常的程式碼模板。因為在平常我們使用完 Connection 後,需要關閉資源,會呼叫 Connection 的 close() 方法,那麼現在使用連線池後,不是真的去關閉資源,而是把 Connection 重新放入池子裡,供下次使用。那麼如果使用如下程式碼:

  	PoolConnction.push(Connection);

這樣顯示放入池子裡不優雅,也不符合正常寫 jdbc 程式碼。因此就有在呼叫 Connection 的 close() 方法時,實際上並不是真的去關閉資源,那麼我們就得需要一種機制知道呼叫的是什麼方法,而動態代理機制剛好可以做到。

我們可以看看 PooledConnection 類的 invoke() 方法:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    String methodName = method.getName();
    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);
      }
    }
}

所有對 javax.sql.Connection 類的方法都會進入此方法中,可以看到,首先會判斷是否呼叫了 close 方法,如果是的話,會執行 dataSource.pushConnection(this) 這行程式碼,做到了對使用者無感知。