SpringBoot HikariCP配置項及源碼解析

前言

在SpringBoot2.0之後,采用的默認數據庫連接池就是Hikari,是一款非常強大,高效,並且號稱“史上最快連接池”。我們知道的連接池有C3P0,DBCP,Druid它們都比較成熟穩定,但性能不是十分好。

我們在日常的編碼中,通常會將一些對象保存起來,這主要考慮的是對象的創建成本;比如像線程資源、數據庫連接資源或者 TCP 連接等,這類對象的初始化通常要花費比較長的時間,如果頻繁地申請和銷毀,就會耗費大量的系統資源,造成不必要的性能損失,於是在Java 中,池化技術應用非常廣泛。在軟件行開發中,軟件的性能是占主導地位的,於是HikariCP就在眾多數據庫連接池中脫穎而出。

為什麼HikariCP性能高

  • 優化代理和攔截器:減少代碼。
  • 字節碼精簡 :優化代碼(HikariCP利用瞭一個第三方的Java字節碼修改類庫Javassist來生成委托實現動態代理,動態代理的實現在ProxyFactory類),直到編譯後的字節碼最少,這樣,CPU緩存可以加載更多的程序代碼。
  • 通過代碼設計和優化大幅減少線程間的鎖競爭。這點主要通過ConcurrentBag來實現。
  • 自定義數組類型(FastStatementList)代替ArrayList:避免每次get()調用都要進行range check,避免調用remove()時的從頭到尾的掃描,相對與ArrayList極大地提升瞭性能,而其中的區別是,ArrayList在每次執行get(Index)方法時,都需要對List的范圍進行檢查,而FastStatementList不需要,在能確保范圍的合法性的情況下,可以省去范圍檢查的開銷。自定義集合類型(ConcurrentBag):支持快速插入和刪除,特別是在同一線程既添加又刪除項時,提高並發讀寫的效率;
  • 關於Connection的操作:另外在Java代碼中,很多都是在使用完之後直接關閉連接,以前都是從頭到尾遍歷,來關閉對應的Connection,而HikariCP則是從尾部對Connection集合進行掃描,整體上來說,從尾部開始的性能更好一些。
  • 針對連接中斷的情況:比其他CP響應時間上有瞭極好的優化,響應時間為5S,會拋出SqlException異常,並且後續的getConnection()可以正常進行

下面為大傢附上一張官方的性能測試圖,我們可以從圖上很直觀的看出HikariCP的性能卓越:

常用配置項

autoCommit

控制從池返回的連接的默認自動提交行為,默認為true

connectionTimeout

控制客戶端等待來自池的連接的最大毫秒數。

如果在沒有連接可用的情況下超過此時間,則將拋出 SQLException。可接受的最低連接超時時間為 250 毫秒。默認值:30000(30 秒)

idleTimeout

連接允許在池中閑置的最長時間

如果idleTimeout+1秒>maxLifetime 且 maxLifetime>0,則會被重置為0(代表永遠不會退出);如果idleTimeout!=0且小於10秒,則會被重置為10秒

這是HikariCP用來判斷是否應該從連接池移除空閑連接的一個重要的配置。負責剔除的也還是HouseKeeper這個定時任務,值為0時,HouseKeeper不會移除空閑連接,直到到達maxLifetime後,才會移除,默認值也就是0。
正常情況下,HouseKeeper會找到所有狀態為空閑的連接隊列,遍歷一遍,將空閑超時到達idleTimeout且未超過minimumIdle數量的連接的批量移除。

maxLifetime

池中連接最長生命周期;如果不等於0且小於30秒則會被重置回30分鐘

瞭解這個值的作用前,先瞭解一下MySQLwait_timeout的作用:MySQL 為瞭防止空閑連接浪費,占用資源,在超過wait_timeout時間後,會主動關閉該連接,清理資源;默認是28800s,也就是8小時。簡而言之就是MySQL會在某個連接超過8小時還沒有任何請求時自動斷開連接,但是HikariCP如何知道池子裡的連接有沒有超過這個時間呢?所以就有瞭maxLifetime,配置後HikariCP會把空閑鏈接超過這個時間的給剔除掉,防止獲取到已經關閉的連接導致異常。

connectionTestQuery

將在從池中向您提供連接之前執行的查詢,以驗證與數據庫的連接是否仍然有效,如select 1

minimumIdle

池中維護的最小空閑連接數;minIdle<0或者minIdle>maxPoolSize,則被重置為maxPoolSize

HikariCP Pool創建時,會啟動一個HouseKeeper定時任務,每隔30s,判斷空閑線程數低於minimumIdle,並且當前線程池總連接數小於maximumPoolSize,就建立和MySQL的一個長連接,然後加入到連接池中。官方建議minimumIdlemaximumPoolSize保持一致。 因為HikariCPHouseKeeper在發現idleTimeout>0 並且 minimumIdle < maximumPoolSize時,先會去掃描一遍需要移除空閑連接,和MySQL斷開連接。然後再一次性補滿空閑連接數至到minimumIdle

maximumPoolSize

池中最大連接數,其實就是線程池中隊列的大小,默認大小為10(包括閑置和使用中的連接)

如果maxPoolSize小於1,則會被重置。當minIdle<=0被重置為DEFAULT_POOL_SIZE則為10;如果minIdle>0則重置為minIdle的值

HikariCP架構

分析源碼之前,先給大傢介紹一下HikariCP的整體架構,整體架構和DBCP2 的有點類似(由此可見 HikariCP 與 DBCP2 性能差異並不是由於架構設計),下面我總結瞭幾點,來和大傢一起探討下:

  • HikariCP通過JMX調用HikariPoolMXBean來獲取連接池的連接數、獲取等待連接的線程數、丟棄未使用連接、掛起和恢復連接池等。
  • HikariCP通過JMX調用HikariConfigMXBean來動態修改配置。
  • HikariCP使用HikariConfig加載配置文件,一般會作為入參來構造 HikariDataSource 對象。
  • HikariPool是一個非常重要的類,它負責管理連接,涉及到比較多的代碼邏輯。
  • HikariDataSource主要用於操作HikariPool獲取連接。
  • ConcurrentBag用於優化大幅減少線程間的鎖競爭。
  • PoolBaseHikariPool的父類,主要負責操作實際的DataSource獲取連接,並設置連接的一些屬性。

源碼解析

HikariConfig

HikariConfig保存瞭所有連接池配置,另外實現瞭HikariConfigMXBean接口,有些配置可以利用JMX運行時變更。核心配置項屬性會在下面給大傢介紹,這邊Dong哥就簡單介紹一下瞭。

HikariPool

getConnection

  public Connection getConnection(final long hardTimeout) throws SQLException
   {
      //這裡是防止線程池處於暫停狀態(通常不允許線程池可暫停)
      suspendResumeLock.acquire();
      final long startTime = currentTime();
      try {
         long timeout = hardTimeout;
         do {
            //PoolEntry 用於跟蹤connection實例,裡面包裝瞭Connection;
            //從connectionBag中獲取一個對象,並且檢測是否可用
            PoolEntry poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
            if (poolEntry == null) {
               break; // We timed out... break and throw exception
            }
            final long now = currentTime();
            //1、已被標記為驅逐 2、已超過最大存活時間 3、鏈接已死
            if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > ALIVE_BYPASS_WINDOW_MS && !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);
		 //超時時間到仍未獲取到鏈接則拋出 TimeoutException
         metricsTracker.recordBorrowTimeoutStats(startTime);
         throw createTimeoutException(startTime);
      }
      catch (InterruptedException e) {
         Thread.currentThread().interrupt();
         throw new SQLException(poolName + " - Interrupted during connection acquisition", e);
      }
      finally {
         suspendResumeLock.release();
      }
   }

校驗

  • isMarkedEvicted:檢查當前鏈接是否已被驅逐
  • elapsedMillis(poolEntry.lastAccessed, now):檢查鏈接是否超過最大存活時間(maxLifetime配置時間)
/**
* startTime 上次使用時間
* endTime   當前時間
*/
static long elapsedMillis(long startTime, long endTime) {
    return CLOCK.elapsedMillis0(startTime, endTime);
}
  • isConnectionAlive:連接是否還是存活狀態
 boolean isConnectionAlive(final Connection connection)
   {
      try {
         try {
            //如果支持Connection networkTimeout,則優先使用並設置
            setNetworkTimeout(connection, validationTimeout);
            final int validationSeconds = (int) Math.max(1000L, validationTimeout) / 1000;
	    //如果jdbc實現支持jdbc4 則使用jdbc4 Connection的isValid方法檢測
            if (isUseJdbc4Validation) {
               return connection.isValid(validationSeconds);
            }
	    //查詢數據庫檢測連接可用性
            try (Statement statement = connection.createStatement()) {
              //如果不支持Connection networkTimeout 則設置Statement queryTimeout
               if (isNetworkTimeoutSupported != TRUE) {
                  setQueryTimeout(statement, validationSeconds);
               }
               statement.execute(config.getConnectionTestQuery());
            }
         }
         finally {
            setNetworkTimeout(connection, networkTimeout);
            if (isIsolateInternalQueries &amp;&amp; !isAutoCommit) {
               connection.rollback();
            }
         }
         return true;
      }
      catch (Exception e) {
         lastConnectionFailure.set(e);
         LOGGER.warn("{} - Failed to validate connection {} ({}). Possibly consider using a shorter maxLifetime value.",
                     poolName, connection, e.getMessage());
         //捕獲到異常,說明鏈接不可用。(connection is unavailable)
         return false;
      }
   }

HouseKeeper

HouseKeeper負責保持,我們始終有minimumIdle空閑鏈接可用

 private final class HouseKeeper implements Runnable
   {
      //默認30s,執行一次
      private volatile long previous = plusMillis(currentTime(), -HOUSEKEEPING_PERIOD_MS);
      @Override
      public void run()
      {
         try {
            //省略......
            String afterPrefix = "Pool ";
            if (idleTimeout &gt; 0L &amp;&amp; config.getMinimumIdle() &lt; config.getMaximumPoolSize()) {
               logPoolState("Before cleanup ");
               afterPrefix = "After cleanup  ";
	       //空閑鏈接數
               final List&lt;PoolEntry&gt; notInUse = connectionBag.values(STATE_NOT_IN_USE);
               int toRemove = notInUse.size() - config.getMinimumIdle();
               for (PoolEntry entry : notInUse) {
                  if (toRemove &gt; 0 &amp;&amp; elapsedMillis(entry.lastAccessed, now) &gt; idleTimeout &amp;&amp; connectionBag.reserve(entry)) {
                     //關閉過多的空閑超時鏈接
                     closeConnection(entry, "(connection has passed idleTimeout)");
                     toRemove--;
                  }
               }
            }
	    //記錄pool狀態信息
            logPoolState(afterPrefix);
	    //補充空閑鏈接
            fillPool(); 
         }
         catch (Exception e) {
            LOGGER.error("Unexpected exception in housekeeping task", e);
         }
      }
   }

HouseKeeper其實是一個線程,也是寫在HikariPool類裡面的一個內部類,主要負責保持 minimumIdle 的空閑鏈接。HouseKeeper也用到瞭validationTimeout, 並且會根據minimumIdle配置,通過fill 或者 remove保持最少空閑鏈接數。
HouseKeeper線程初始化:

 public HikariPool(final HikariConfig config)
   {
      super(config);
      this.connectionBag = new ConcurrentBag<>(this);
      this.suspendResumeLock = config.isAllowPoolSuspension() ? new SuspendResumeLock() : SuspendResumeLock.FAUX_LOCK;
      //執行初始化
      this.houseKeepingExecutorService = initializeHouseKeepingExecutorService();
      //省略......
      }
   }
private ScheduledExecutorService initializeHouseKeepingExecutorService() {
        if (this.config.getScheduledExecutor() == null) {
            ThreadFactory threadFactory = (ThreadFactory)Optional.ofNullable(this.config.getThreadFactory()).orElseGet(() -> {
                return new DefaultThreadFactory(this.poolName + " housekeeper", true);
            });
            //ScheduledThreadPoolExecutor是ThreadPoolExecutor類的子類,Java推薦僅在開發定時任務程序時采用ScheduledThreadPoolExecutor類
            ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1, threadFactory, new DiscardPolicy());
            //傳入false,則執行shutdown()方法之後,待處理的任務將不會被執行
            executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
            //取消任務後,判斷是否需要從阻塞隊列中移除任務
            executor.setRemoveOnCancelPolicy(true);
            return executor;
        } else {
            return this.config.getScheduledExecutor();
        }
    }

HikariDataSource

HikariDataSource 非常重要,主要用於操作HikariPool獲取連接,並且能夠清除空閑連接。

public class HikariDataSource extends HikariConfig implements DataSource, Closeable {
   private final AtomicBoolean isShutdown = new AtomicBoolean();
   //final修飾,構造時決定,如果使用無參構造為null,使用有參構造和pool一樣
   private final HikariPool fastPathPool;
   //volatile修飾,無參構造不會設置pool,在getConnection時構造pool,有參構造和fastPathPool一樣。
   private volatile HikariPool pool;
   public HikariDataSource() {
      super();
      fastPathPool = null;
   }
   public HikariDataSource(HikariConfig configuration) {
      configuration.validate();
      configuration.copyStateTo(this);
      pool = fastPathPool = new HikariPool(this);
      this.seal();
   }
}

以上就是SpringBoot HikariCP配置項及源碼解析的詳細內容,更多關於SpringBoot HikariCP配置的資料請關註WalkonNet其它相關文章!

推薦閱讀: