01Hikari 源碼解析之connection相關

一、初始化

類 HikariDataSource 的 getConnection() 爲 取connection 的方法,主要流程如下:

  1. 判斷 當前的 dataSource 是否關閉,如果關閉 ,拋出異常
  2. 判斷fastPathPool 是否爲null ,默認爲null
  3. 判斷當前的pool 是否爲null ,如果爲空,那就就行初始化,所以項目啓動完了之後,第一次請求時間比較長
  4. 不爲空 ,返回 result.getConnection()
public Connection getConnection() throws SQLException{
      if (isClosed()) {
         throw new SQLException("HikariDataSource " + this + " has been closed.");
      }
      if (fastPathPool != null) {
         return fastPathPool.getConnection();
      }
      // See http://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java
      HikariPool result = pool;
      if (result == null) {
         synchronized (this) {
            result = pool;
            ...  ... ...... 省略,詳細過程在下面分析
            xxxx .... xxxxx 
            }
      }
 }         

第一次獲取connection的時候,需要初始化,大致流程如下:
在這裏插入圖片描述

  1. 設置poolName ,前綴 HikariPool- ,後面跟的是 pool_number
  2. validateNumerics,這裏是 對一些參數校驗,具體 查看下面的表格
  3. 對 pool 進行初始,裏面還有一系列的 參數初始化 ,這裏暫不展開,主要涉及houseKeeperTask ,這個是 在delay 100ms 之後 每隔 30秒 定時運行,詳細在下面概述
    在這裏插入圖片描述

1.1 houseKeeperTask 定時清理 和 填充連接池

在 初始化的時候, houseKeeperTask 設置爲 initialDelay = 100L, 每隔30S(默認) 運行一次

this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, housekeepingPeriodMs, MILLISECONDS);

主要邏輯在 內部類 HouseKeeper的run() 方法裏面,看一下主要的邏輯,就是不斷的維護線程池裏面的連接數

public void run() {
         try {
    ... 省略 ...
             previous = now;
            String afterPrefix = "Pool ";
            // 如果idleTimeout >0 ,默認10分鐘,600000ms 並且 最小連接數 小於最大連接數
            // 如果不配置 最小連接數 ,最小連接數 默認和 max_num_size 一樣大,都是默認10
            if (idleTimeout > 0L && config.getMinimumIdle() < config.getMaximumPoolSize()) {
               logPoolState("Before cleanup ");
               afterPrefix = "After cleanup  ";

               final List<PoolEntry> notInUse = connectionBag.values(STATE_NOT_IN_USE);
               int toRemove = notInUse.size() - config.getMinimumIdle();
               // 刪除超時的連接
               for (PoolEntry entry : notInUse) {
                  if (toRemove > 0 && elapsedMillis(entry.lastAccessed, now) > idleTimeout && connectionBag.reserve(entry)) {
                     closeConnection(entry, "(connection has passed idleTimeout)");
                     toRemove--;
                  }
               }
            }

            logPoolState(afterPrefix);
            //進行連接池補充
            fillPool(); // Try to maintain minimum connections
         }
         catch (Exception e) {
            LOGGER.error("Unexpected exception in housekeeping task", e);
         }
      }
      
private synchronized void fillPool()
   {
      final int connectionsToAdd = Math.min(config.getMaximumPoolSize() - getTotalConnections(), config.getMinimumIdle() - getIdleConnections())
                                   - addConnectionQueue.size();
      for (int i = 0; i < connectionsToAdd; i++) {
         addConnectionExecutor.submit((i < connectionsToAdd - 1) ? POOL_ENTRY_CREATOR : POST_FILL_POOL_ENTRY_CREATOR);
      }
   }

1.2 創建連接 createPoolEntry

這裏主要介紹一下 createPoolEntry 方法,重點就是 創建了一個 poolEntry之後,在到 maxlifetime 最大還差2.5% 時,進行標記,如果 當前空閒直接回收,如果回收成功,在判斷一下是否需要新建一個poolEntry

private PoolEntry createPoolEntry()
   {
      try {
         final PoolEntry poolEntry = newPoolEntry();

         final long maxLifetime = config.getMaxLifetime();
         if (maxLifetime > 0) {
            // variance up to 2.5% of the maxlifetime
            final long variance = maxLifetime > 10_000 ? ThreadLocalRandom.current().nextLong( maxLifetime / 40 ) : 0;
            final long lifetime = maxLifetime - variance;
            // 快接近最大存活時間時,對連接進行標記,如果當前連接空閒,直接回收
            如果回收成功,再判斷是否有等待線程,如果有說明當前資源緊張,再創建一個poolEntry
            poolEntry.setFutureEol(houseKeepingExecutorService.schedule(
               () -> {
                  if (softEvictConnection(poolEntry, "(connection has passed maxLifetime)", false /* not owner */)) {
                     addBagItem(connectionBag.getWaitingThreadCount());
                  }
               },
               lifetime, MILLISECONDS));
         }

         return poolEntry;
      }
      catch (Exception e) {
         if (poolState == POOL_NORMAL) { // we check POOL_NORMAL to avoid a flood of messages if shutdown() is running concurrently
            LOGGER.debug("{} - Cannot acquire connection from data source", poolName, (e instanceof ConnectionSetupException ? e.getCause() : e));
         }
         return null;
      }
   }
   
     public void addBagItem(final int waiting)
   {
      final boolean shouldAdd = waiting - addConnectionQueue.size() >= 0; // Yes, >= is intentional.
      if (shouldAdd) {
         addConnectionExecutor.submit(POOL_ENTRY_CREATOR);
      }
   }

二、獲取Connection

上面已經大致涉及第一次初始化的 過程,接下來概述一些 getConnection的流程,主要是在類HikariPool 的
getConnection() 裏面,代碼如下,

public Connection getConnection(final long hardTimeout) throws SQLException
   {
   // 獲取信號量,最大值1000
   suspendResumeLock.acquire();
   final long startTime = currentTime();
   try {
      long timeout = hardTimeout;
      do {
         // 從connectionBag 裏面 獲取borrow
         PoolEntry poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
         if (poolEntry == null) {
            break; // We timed out... break and throw exception
         }
         final long now = currentTime();
         // 判斷當前的poolEntry 是否有效,無效就關閉
         if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > aliveBypassWindowMs && !isConnectionAlive(poolEntry.connection))) {
            closeConnection(poolEntry, poolEntry.isMarkedEvicted() ? EVICTED_CONNECTION_MESSAGE : DEAD_CONNECTION_MESSAGE);
            timeout = hardTimeout - elapsedMillis(startTime);
         }
         else {
            metricsTracker.recordBorrowStats(poolEntry, startTime);
            return poolEntry.createProxyConnection(leakTaskFactory.schedule(poolEntry), now);
         }
      } while (timeout > 0L);  // 在connectionTimeout 時間內 一直循環直到找到可用的

      metricsTracker.recordBorrowTimeoutStats(startTime);
      throw createTimeoutException(startTime);
   }
   catch (InterruptedException e) {
      Thread.currentThread().interrupt();
      throw new SQLException(poolName + " - Interrupted during connection acquisition", e);
   }
   finally {
      suspendResumeLock.release();
   }
}

其流程圖如下:
在這裏插入圖片描述
大致流程如下:

  1. 獲取信號量suspendResumeLock ,這裏 默認是 1000個

  2. 從connectionBag 裏面獲取 poolEntry ,這裏 做了一定的優化 ,ConcurrentBag(borrow)詳細的見02Hikari源碼解析之ConcurrentBag、FastList分析 ,這裏概述一下:
    2.1. 先從 threadList 裏面獲取,同一個Request 即同一個線程時 從裏面獲取
    2.2. 如果threadList 裏面可用的 ,再從sharedList 裏面獲取 ,sharedList 存放ConcurrentBag中全部用於出借的資源 ,這裏有一個注意點,這裏也是 hkari 快的原因,就是判斷如果 還有其他的等待者,趕緊的再去創建連接,而沒有判斷 當前 sharedList 包裏面 是否夠用了,所以 高QPS的情況下,很容易連接池達到最大的量
    2.3. 當現有全部資源全部在使用中,等待一個被釋放的資源或者一個新資源,handoffQueue裏面獲取

  3. 判斷 poolEntity 是否被標記驅逐 或者 是否connectionAlive 以及上一次訪問時間到當前是否 大於 aliveBypassWindowMs(默認 500ms)

  4. 如果是有效連接,便開始 創建createProxyConnection

  5. 否則 繼續循環 2-3 的 步驟,直到 connectionTimeout 用完

三、歸還Connection

邏輯代碼主要在 HikariPool 類下面的 recycle(final PoolEntry poolEntry) ,流程圖如下:
主要邏輯:
1.先將裏面的所有Statement 關閉,並將保存的list 對象openStatements 清空
2.將 代理delegate 狀態設置爲 CLOSE_CONNECTION
3.將 poolEntity 的狀態設置爲 STATE_NOT_IN_USE
4.判斷是否有 等待線程,如果有 直接推到 handoffQueue 隊列 進行第一時間的交換
5.否則放入 本地 threadLocalList ,默認是弱引用,這樣不影響回收
在這裏插入圖片描述

四、總結

Hikari 在獲取連接的時候,做了一定的優化:

首先 查看 是否有可用的連接,從 ThreadLocal<List> 裏面獲取,這裏 的 list ,是自定義了一個 FastList,而 FastList 和普通的ArrayList 不同之處就是 Fast 在get() 和remove () 方面去掉了rangeCheck(index); 一定程度上又快了一些.
全部資源用了 CopyOnWriteArrayList 定義的 sharedList ,這樣讀的是沒有鎖,寫的時候進行了 加鎖操作
SynchronousQueue 是使用了 公平鎖 機制, SynchronousQueue 就是一定程度上無緩存的隊列.
Hikari 作者 不推薦使用minimumIdle,該屬性控制HikariCP嘗試在池中維護的最小空閒連接數。如果空閒連接低於此值並且池中的總連接數少於maximumPoolSize,HikariCP將盡最大努力快速高效地添加其他連接。但是,爲了獲得最佳性能和響應尖峯需求,我們建議不要設置此值,而是允許HikariCP充當固定大小的連接池。

Hikari 作者 認爲如果minimumIdle小於maximumPoolSize的話,在流量激增的時候需要額外的連接,此時在請求方法裏頭再去處理新建連接會造成性能損失,即會導致數據庫一方面降低連接建立的速度,另一方面也會影響既有的連接事務的完成,間接影響了這些既有連接歸還到連接池的速度。 Hikari 作者 認爲minimumIdle與maximumPoolSize設置成一樣,多餘的空閒連接不會對整體的性能有什麼嚴重影響。

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