連接池-Mybatis源碼

持續更新:https://github.com/dchack/Mybatis-source-code-learn

Mybatis連接池

有這麼個定律,有連接的地方就有池。
在市面上,可以適配Mybatis DateSource的連接池有很對,比如:

Mybatis也自帶來連接池的功能,先學習下Mybatis的,相對簡單的實現。
涉及的類:

PoolState
public class PoolState {

  protected PooledDataSource dataSource;
  // 空閒連接集合
  protected final List<PooledConnection> idleConnections = new ArrayList<PooledConnection>();
  // 正在使用的連接集合
  protected final List<PooledConnection> activeConnections = new ArrayList<PooledConnection>();
  // 請求次數,每次獲取連接,都會自增,用於
  protected long requestCount = 0;
  // 累計請求耗時,每次獲取連接時計算累加,除以requestCount可以獲得平均耗時
  protected long accumulatedRequestTime = 0;
  // 累計連接使用時間
  protected long accumulatedCheckoutTime = 0;
  // 過期連接次數
  protected long claimedOverdueConnectionCount = 0;
  protected long accumulatedCheckoutTimeOfOverdueConnections = 0;
  // 累計等待獲取連接時間
  protected long accumulatedWaitTime = 0;
  // 等待獲取連接的次數
  protected long hadToWaitCount = 0;
  // 連接已關閉的次數
  protected long badConnectionCount = 0;

  public PoolState(PooledDataSource dataSource) {
    this.dataSource = dataSource;
  }

  public synchronized long getRequestCount() {
    return requestCount;
  }

  public synchronized long getAverageRequestTime() {
    return requestCount == 0 ? 0 : accumulatedRequestTime / requestCount;
  }

  public synchronized long getAverageWaitTime() {
    return hadToWaitCount == 0 ? 0 : accumulatedWaitTime / hadToWaitCount;

  }

  public synchronized long getHadToWaitCount() {
    return hadToWaitCount;
  }

  public synchronized long getBadConnectionCount() {
    return badConnectionCount;
  }

  public synchronized long getClaimedOverdueConnectionCount() {
    return claimedOverdueConnectionCount;
  }

  public synchronized long getAverageOverdueCheckoutTime() {
    return claimedOverdueConnectionCount == 0 ? 0 : accumulatedCheckoutTimeOfOverdueConnections / claimedOverdueConnectionCount;
  }

  public synchronized long getAverageCheckoutTime() {
    return requestCount == 0 ? 0 : accumulatedCheckoutTime / requestCount;
  }


  public synchronized int getIdleConnectionCount() {
    return idleConnections.size();
  }

  public synchronized int getActiveConnectionCount() {
    return activeConnections.size();
  }
}

注意代碼中的字段都是用protected修飾的,表示pooled包內都可訪問,在寫這份代碼的時候必然默認這個包下實現一個獨立的功能,內部字段都可以共享使用,否則都寫set,get方法太麻煩了。
PoolState類中,很多指標比如requestCountclaimedOverdueConnectionCount等都不和連接池核心邏輯相關,純粹只是表示連接池的一些指標而已。
作爲連接池,在這裏最重要的就是兩個List:

  • idleConnections
  • activeConnections
    這兩個都是ArrayList,所以在整個實現中我們是通過synchronized關鍵字來處理併發場景的。
PooledConnection

組成池的兩個List中存儲的是PooledConnection,而PooledConnection通過java動態代理機制實現代理真正Connection。
PooledConnection繼承InvocationHandler,所以實現了invoke方法:

  /*
   * Required for InvocationHandler implementation.
   *
   * @param proxy  - not used
   * @param method - the method to be executed
   * @param args   - the parameters to be passed to the method
   * @see java.lang.reflect.InvocationHandler#invoke(Object, java.lang.reflect.Method, Object[])
   */
  @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())) {
          // 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);
      }
    }
  }

  private void checkConnection() throws SQLException {
    if (!valid) {
      throw new SQLException("Error accessing PooledConnection. Connection is invalid.");
    }
  }

主要看到這個代理實現處理了close方法,就是將連接從使用列表中彈出。
對於其他方法,會判斷方法是否屬於Object中的方法,如果不是則進行連接合法的校驗,然後執行真正ConnectionrealConnection中對應的方法。
獲得一個代理類的代碼,即調用Proxy.newProxyInstance方法,在PooledConnection中的構造函數中:

  /*
   * Constructor for SimplePooledConnection that uses the Connection and PooledDataSource passed in
   *
   * @param connection - the connection that is to be presented as a pooled connection
   * @param dataSource - the dataSource that the connection is from
   */
  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);
  }

我們可以看到realConnection是在構造函數時就傳入的了。

而配置這個池的參數都是在PooledDataSource中:

官方文檔:
poolMaximumActiveConnections – 在任意時間可以存在的活動(也就是正在使用)連接數量,默認值:10
poolMaximumIdleConnections – 任意時間可能存在的空閒連接數。
poolMaximumCheckoutTime – 在被強制返回之前,池中連接被檢出(checked out)時間,默認值:20000 毫秒(即 20 秒)
poolTimeToWait – 這是一個底層設置,如果獲取連接花費了相當長的時間,連接池會打印狀態日誌並重新嘗試獲取一個連接(避免在誤配置的情況下一直安靜的失敗),默認值:20000 毫秒(即 20 秒)。
poolMaximumLocalBadConnectionTolerance – 這是一個關於壞連接容忍度的底層設置, 作用於每一個嘗試從緩存池獲取連接的線程。 如果這個線程獲取到的是一個壞的連接,那麼這個數據源允許這個線程嘗試重新獲取一個新的連接,但是這個重新嘗試的次數不應該超過 poolMaximumIdleConnections 與 poolMaximumLocalBadConnectionTolerance 之和。 默認值:3 (新增於 3.4.5)
poolPingQuery – 發送到數據庫的偵測查詢,用來檢驗連接是否正常工作並準備接受請求。默認是“NO PING QUERY SET”,這會導致多數數據庫驅動失敗時帶有一個恰當的錯誤消息。
poolPingEnabled – 是否啓用偵測查詢。若開啓,需要設置 poolPingQuery 屬性爲一個可執行的 SQL 語句(最好是一個速度非常快的 SQL 語句),默認值:false。
poolPingConnectionsNotUsedFor – 配置 poolPingQuery 的頻率。可以被設置爲和數據庫連接超時時間一樣,來避免不必要的偵測,默認值:0(即所有連接每一時刻都被偵測 — 當然僅當 poolPingEnabled 爲 true 時適用)。

PooledDataSource

PooledDataSource完成池功能的類,直接看拿連接的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()) {
          // Pool has available connection
          conn = state.idleConnections.remove(0);
          if (log.isDebugEnabled()) {
            log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
          }
        } else {
          // Pool does not have available connection
          // 判斷是否達到最大連接數限制
          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 {
            // Cannot create new connection
            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) {
                  /*
                     Just log a message for debug and continue to execute the following
                     statement like nothing happend.
                     Wrap the bad connection with a new PooledConnection, this will help
                     to not intterupt current executing thread and give current thread a
                     chance to join the next competion 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");
                }  
              }
              // 這裏看到將包裝在oldestActiveConnection中的RealConnection重新用PooledConnection包裝出來直接使用,看前面操作是將連接進行回滾,但是可能失敗,卻不關心,註釋解釋是,在後面的代碼中會進行isValid的判斷,其中就會判斷連接是否可用。
              conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
              conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
              conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
              // 將老連接設置成invalid 
              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++;
            // 不可用的連接會被設置成null,被回收器回收
            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;
  }

popConnection方法實現在一個池中獲取連接的基本邏輯,依賴最大連接數,獲取等待時間,連接使用超時時間等來完成一個池的核心能力。
注意這裏使用wait方法來等待,在java線程池中使用阻塞隊列來出來暫時拿不到資源的請求。

前面提到,在使用Connection時,調用close方法,會調用到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++;
      }
    }
  }

歸還連接時,需要查看空閒列表中的線程數量是否已經到到設置的最大值,如果已經達到,就不需要歸還了,凡是需要加入空閒列表的都需要進行notifyAll操作,來通知那些等待的線程來搶這個歸還的連接,但是如果此時連接池中空閒連接充足,並沒有線程等待,這個操作也就浪費了,所以可以思考前面popConnection中的wait和這裏的notifyAll是可以用等待隊列來完成。

另外一個方法,用於判斷連接是否可用:

 protected boolean pingConnection(PooledConnection conn) {
    boolean result = true;

    try {
      // 先用isClosed來獲取結果
      result = !conn.getRealConnection().isClosed();
    } catch (SQLException e) {
      if (log.isDebugEnabled()) {
        log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
      }
      result = false;
    }

    if (result) {
      // 可以通過poolPingEnabled配置來決定是否使用自定義sql
      if (poolPingEnabled) {
        if (poolPingConnectionsNotUsedFor >= 0 && conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor) {
          try {
            if (log.isDebugEnabled()) {
              log.debug("Testing connection " + conn.getRealHashCode() + " ...");
            }
            Connection realConn = conn.getRealConnection();
            Statement statement = realConn.createStatement();
            // 執行poolPingQuery
            ResultSet rs = statement.executeQuery(poolPingQuery);
            rs.close();
            statement.close();
            if (!realConn.getAutoCommit()) {
              realConn.rollback();
            }
            result = true;
            if (log.isDebugEnabled()) {
              log.debug("Connection " + conn.getRealHashCode() + " is GOOD!");
            }
          } catch (Exception e) {
            log.warn("Execution of ping query '" + poolPingQuery + "' failed: " + e.getMessage());
            try {
              conn.getRealConnection().close();
            } catch (Exception e2) {
              //ignore
            }
            result = false;
            if (log.isDebugEnabled()) {
              log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
            }
          }
        }
      }
    }
    return result;
  }

從代碼中可以看到isClosed方法並不可靠,最終還是通過執行sql來判斷連接是否可用,這個在很多涉及判斷數據庫連接是否有效的地方都是這麼做的,詳細可以看一下isClosed方法的註釋。

PooledDataSourceFactory

繼承UnpooledDataSourceFactory,直接返回PooledDataSource對象

public class PooledDataSourceFactory extends UnpooledDataSourceFactory {

  public PooledDataSourceFactory() {
    this.dataSource = new PooledDataSource();
  }

}
心得

在整個線程池的實現代碼中,可以學習到一個池的實現的要素有哪些,以及錄用基礎代碼如何實現一個池。對於那些封裝成高層次的池的代碼來說,這個實現顯得又些單薄和不夠全面,可是無論連接池如何實現核心池的實現邏輯是不會變的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章