Hikari連線池原始碼解讀

大雄45發表於2022-07-29
導讀 幾年前,我最開始接觸的資料庫連線池是 C3P0,後來是阿里的 Druid,但隨著 Springboot 2.0 選擇 HikariCP 作為預設資料庫連線池這一事件之後,HikariCP 作為一個後起之秀出現在大眾的視野中,以其速度快,效能高等特點受到越來越多人青睞。
寫在前面

幾年前,我最開始接觸的資料庫連線池是 C3P0,後來是阿里的 Druid,但隨著 Springboot 2.0 選擇 HikariCP 作為預設資料庫連線池這一事件之後,HikariCP 作為一個後起之秀出現在大眾的視野中,以其速度快,效能高等特點受到越來越多人青睞。

在實際開發工作中,資料庫一直是引發報警的重災區,而與資料庫打交道的就是 Hikari 連線池,看懂 Hikari 報警日誌並定位異常原因,是實際工作中必不可少的技能!

本文以 Hikari 2.7.9 版本原始碼進行分析,帶大家理解 Hikari 原理,學會處理線上問題!

原始碼地址:

1、概念釋義

在學習一項技術之前,需要先在宏觀的層面去看到它的位置,比如我們今天學習的 HikariCP,它在什麼位置?

Hikari連線池原始碼解讀Hikari連線池原始碼解讀

以 Spring Boot 專案為例,我們有 Service 業務層,編寫業務程式碼,而與資料庫打交道的是 ORM 框架(例如 MyBatis),ORM 框架的下一層是 Hikari 連線池,Hikari 連線池的下一層是 MySQL 驅動,MySQL 驅動的下一層是 MySQL 伺服器。理解了這個宏觀層次,我們再去學習 Hikari 就不會學的那麼稀裡糊塗了。

其次,我們需要明白資料庫連線池是幹什麼的?

簡單來說,資料庫連線池負責分配、管理和釋放資料庫的連線。有了資料庫連線池就可以複用資料庫連線,可以避免連線頻繁建立、關閉的開銷,提升系統的效能。它可以幫助我們釋放過期的資料庫連線,避免因為使用過期的資料庫連線而引起的異常。

至於 Hikari,它是一個“零開銷”生產就緒的 JDBC 連線池。庫非常輕,大約 130 Kb。

2、配置使用

我們先來看一個線上 Hikari 連線池配置需要哪些引數。

@Bean("dataSource")
public DataSource dataSource() {
    HikariConfig cfg = new HikariConfig();
    // 從池中借出的連線是否預設自動提交事務,預設開啟
    cfg.setAutoCommit(false);
    // 從池中獲取連線時的等待時間
    cfg.setConnectionTimeout();
    // MYSQL連線相關
    cfg.setJdbcUrl();
    cfg.setDriverClassName();
    cfg.setUsername();
    cfg.setPassword();
    // 連線池的最大容量
    cfg.setMaximumPoolSize();
    // 連線池的最小容量,官網不建議設定,保持與 MaximumPoolSize 一致,從而獲得最高效能和對峰值需求的響應
    // cfg.setMinimumIdle();
    // 連線池的名稱,用於日誌監控,多資料來源要區分
    cfg.setPoolName();
    // 池中連線的最長存活時間,要比資料庫的 wait_timeout 時間要小不少
    cfg.setMaxLifetime();
    // 連線在池中閒置的最長時間,僅在 minimumIdle 小於 maximumPoolSize 時生效(本配置不生效)
    cfg.setIdleTimeout();
    // 連線洩露檢測,預設 0 不開啟
    // cfg.setLeakDetectionThreshold();
    // 測試連結是否有效的超時時間,預設 5 秒
    // cfg.setValidationTimeout();
    // MYSQL驅動環境變數
    // 字元編解碼
    cfg.addDataSourceProperty("characterEncoding", );
    cfg.addDataSourceProperty("useUnicode", );
    // 較新版本的 MySQL 支援伺服器端準備好的語句
    cfg.addDataSourceProperty("useServerPrepStmts", );
    // 快取SQL開關
    cfg.addDataSourceProperty("cachePrepStmts", );
    // 快取SQL數量
    cfg.addDataSourceProperty("prepStmtCacheSize", );
    // 快取SQL長度,預設256
    // prepStmtCacheSqlLimit
    return new HikariDataSource(cfg);
}

官方配置說明:

3、原始碼分析
1)分析入口

萬事開頭難,下載 Hikari 原始碼到本地後該從哪開始去看呢?不妨從下面兩個入口去分析。

// 1、初始化入口
new HikariDataSource(cfg)
// 2、獲取連線
public interface DataSource  extends CommonDataSource, Wrapper {
  Connection getConnection() throws SQLException;
}
2)初始化分析

初始化分析主要有兩部分工作,一是校驗配置並且會矯正不符合規範的配置;二是例項化 Hikari 連線池。

public HikariDataSource(HikariConfig configuration)
   {
      // 1、校驗配置 並 矯正配置
      configuration.validate();
      configuration.copyStateTo(this);
      LOGGER.info("{} - Starting...", configuration.getPoolName());
      // 2、建立連線池,注意這裡設定了 fastPathPool 
      pool = fastPathPool = new HikariPool(this);
      LOGGER.info("{} - Start completed.", configuration.getPoolName());
      this.seal();
   }

矯正配置
校驗配置會直接拋異常,大部分坑來源於矯正配置這一步,這會使你的配置不生效。

private void validateNumerics() {
      // maxLifetime 連結最大存活時間最低30秒,小於30秒不生效
      if (maxLifetime != 0 && maxLifetime < SECONDS.toMillis(30)) {
         LOGGER.warn("{} - maxLifetime is less than 30000ms, setting to default {}ms.", poolName, MAX_LIFETIME);
         maxLifetime = MAX_LIFETIME;
      }
      // idleTimeout 空閒超時不能大於或者接近 maxLifetime,否則設定 0,禁用空閒執行緒回收
      if (idleTimeout + SECONDS.toMillis(1) > maxLifetime && maxLifetime > 0) {
         LOGGER.warn("{} - idleTimeout is close to or more than maxLifetime, disabling it.", poolName);
         idleTimeout = 0;
      }
      // idleTimeout 空閒超時不能低於預設值 10 秒
      if (idleTimeout != 0 && idleTimeout < SECONDS.toMillis(10)) {
         LOGGER.warn("{} - idleTimeout is less than 10000ms, setting to default {}ms.", poolName, IDLE_TIMEOUT);
         idleTimeout = IDLE_TIMEOUT;
      }
      // 連線洩露檢測的時間,預設 0 不開啟,不能低於 2 秒,不能比 maxLifetime 大,否則不開啟
      if (leakDetectionThreshold > 0 && !unitTest) {
         if (leakDetectionThreshold < SECONDS.toMillis(2) || (leakDetectionThreshold > maxLifetime && maxLifetime > 0)) {
            LOGGER.warn("{} - leakDetectionThreshold is less than 2000ms or more than maxLifetime, disabling it.", poolName);
            leakDetectionThreshold = 0;
         }
      }
      // 從連線池獲取連線時最大等待時間,預設值 30 秒, 低於 250 毫秒不生效
      if (connectionTimeout < 250) {
         LOGGER.warn("{} - connectionTimeout is less than 250ms, setting to {}ms.", poolName, CONNECTION_TIMEOUT);
         connectionTimeout = CONNECTION_TIMEOUT;
      }
      // 檢測連線是否有效的超時時間,預設 5 秒,低於 250 毫秒不生效
      if (validationTimeout < 250) {
         LOGGER.warn("{} - validationTimeout is less than 250ms, setting to {}ms.", poolName, VALIDATION_TIMEOUT);
         validationTimeout = VALIDATION_TIMEOUT;
      }
      // 連線池中連線的最大數量,minIdle 大於 0 與其保持一致,否則預設 10
      if (maxPoolSize < 1) {
         maxPoolSize = (minIdle <= 0) ? DEFAULT_POOL_SIZE : minIdle;
      }
      // 維持的最小連線數量,不配置預設等於 maxPoolSize
      if (minIdle < 0 || minIdle > maxPoolSize) {
         minIdle = maxPoolSize;
      }
   }

建立連線池
透過分析連線池例項化過程,可以看到 Hikari 的作者是多麼喜歡用非同步操作了,包括空閒執行緒處理、新增連線、關閉連線、連線洩露檢測等。

這一步會建立 1 個 LinkedBlockQueue 阻塞佇列,需要明確的是,這個佇列並不是實際連線池的佇列, 只是用來放置新增連線的請求。

public HikariPool(final HikariConfig config)
   {
      super(config);
      // 建立 ConcurrentBag 管理連線池,有連線池的四個重要操作:borrow獲取連線,requite歸還連線,add新增連線,remove移除連線。
      this.connectionBag = new ConcurrentBag<>(this);
      // getConnection 獲取連線時的併發控制,預設關閉
      this.suspendResumeLock = config.isAllowPoolSuspension() ? new SuspendResumeLock() : SuspendResumeLock.FAUX_LOCK;
      // 空閒執行緒池 處理定時任務
      this.houseKeepingExecutorService = initializeHouseKeepingExecutorService();
      // 快速預檢查 建立1個連結
      checkFailFast();
      // Metrics 監控收集相關
      if (config.getMetricsTrackerFactory() != null) {
         setMetricsTrackerFactory(config.getMetricsTrackerFactory());
      }
      else {
         setMetricRegistry(config.getMetricRegistry());
      }
      // 健康檢查註冊相關,預設 無
      setHealthCheckRegistry(config.getHealthCheckRegistry());
      // 處理JMX監控相關
      registerMBeans(this);
      ThreadFactory threadFactory = config.getThreadFactory();
      // 建立 maxPoolSize 大小的 LinkedBlockQueue 阻塞佇列,用來構造 addConnectionExecutor
      LinkedBlockingQueueaddConnectionQueue = new LinkedBlockingQueue<>(config.getMaximumPoolSize());
      // 映象只讀佇列
      this.addConnectionQueue = unmodifiableCollection(addConnectionQueue);
      // 建立 新增連線的 執行緒池,實際執行緒數只有1,拒絕策略是丟棄不處理
      this.addConnectionExecutor = createThreadPoolExecutor(addConnectionQueue, poolName + " connection adder", threadFactory, new ThreadPoolExecutor.DiscardPolicy());
      // 建立 關閉連線的 執行緒池,實際執行緒數只有1,拒絕策略是呼叫執行緒同步執行
      this.closeConnectionExecutor = createThreadPoolExecutor(config.getMaximumPoolSize(), poolName + " connection closer", threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
      // 建立 檢測連線洩露 的工廠,使用的時候只需要傳1個連線物件
      this.leakTaskFactory = new ProxyLeakTaskFactory(config.getLeakDetectionThreshold(), houseKeepingExecutorService);
      // 延時100ms後,開啟任務,每30s執行空閒執行緒處理
      this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, HOUSEKEEPING_PERIOD_MS, MILLISECONDS);
   }
3)獲取連線分析

Hikari 的連線獲取分為兩步,一是呼叫 connectionBag.borrow() 方法從池中獲取連線,這裡等待超時時間是 connectionTimeout;二是獲取連線後,會主動檢測連線是否可用,如果不可用會關閉連線,連線可用的話會繫結一個定時任務用於連線洩露的檢測。

很多時候,會在異常日誌中看到 Connection is not available 錯誤日誌後攜帶的 request timed out 耗時遠超 connectionTimeout,仔細分析原始碼這也是合理的。

HikariDataSource

@Override
   public Connection getConnection() throws SQLException
   {
      if (isClosed()) {
         throw new SQLException("HikariDataSource " + this + " has been closed.");
      }
      // 因為初始化 HikariDataSource 的時候已經設定了,所以這裡直接走 return
      if (fastPathPool != null) {
         return fastPathPool.getConnection();
      }
      // See 
      HikariPool result = pool;
      if (result == null) {
         synchronized (this) {
            result = pool;
            if (result == null) {
               validate();
               LOGGER.info("{} - Starting...", getPoolName());
               try {
                  pool = result = new HikariPool(this);
                  this.seal();
               }
               catch (PoolInitializationException pie) {
                  if (pie.getCause() instanceof SQLException) {
                     throw (SQLException) pie.getCause();
                  }
                  else {
                     throw pie;
                  }
               }
               LOGGER.info("{} - Start completed.", getPoolName());
            }
         }
      }
      return result.getConnection();
   }
HikariPool
   public Connection getConnection() throws SQLException
   {
      // 這裡傳了設定的連結超時
      return getConnection(connectionTimeout);
   }
   public Connection getConnection(final long hardTimeout) throws SQLException
   {
      suspendResumeLock.acquire(); // 併發數量控制,預設關閉
      final long startTime = currentTime();
      try {
         long timeout = hardTimeout;
         do {
            // 此處等待 connectionTimeout ,獲取不到拋異常
            PoolEntry poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
            if (poolEntry == null) {
               break; // We timed out... break and throw exception
            }
            
            final long now = currentTime();
            // 移除已經標記為廢棄的連線 或者 空閒超過 500 毫秒且不可用的連線(超時時間是 validationTimeout,預設5秒)
            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);
               // 先新增連線洩露檢測任務,再透過Javassist建立代理連線
               return poolEntry.createProxyConnection(leakTaskFactory.schedule(poolEntry), now);
            }
         } while (timeout > 0L);
         metricsTracker.recordBorrowTimeoutStats(startTime);
         // 拋異常 Connection is not available, request timed out after {}ms.
         throw createTimeoutException(startTime);
      }
      catch (InterruptedException e) {
         Thread.currentThread().interrupt();
         throw new SQLException(poolName + " - Interrupted during connection acquisition", e);
      }
      finally {
         suspendResumeLock.release();
      }
   }
4)空閒連線回收

Hikari 在初始化連線池的時候,就已經開啟了一條非同步定時任務。該任務每 30 秒執行一次空閒連線回收,程式碼如下:

/**
    * The house keeping task to retire and maintain minimum idle connections.
    * 用於補充和移除最小空閒連線的管理任務。
    */
   private final class HouseKeeper implements Runnable
   {
      private volatile long previous = plusMillis(currentTime(), -HOUSEKEEPING_PERIOD_MS);
      @Override
      public void run()
      {
         try {
            // refresh timeouts in case they changed via MBean
            connectionTimeout = config.getConnectionTimeout();
            validationTimeout = config.getValidationTimeout();
            leakTaskFactory.updateLeakDetectionThreshold(config.getLeakDetectionThreshold());
            final long idleTimeout = config.getIdleTimeout();
            final long now = currentTime();
            // Detect retrograde time, allowing +128ms as per NTP spec.
            // 為了防止時鐘回撥,給了128ms的gap,正常情況下,ntp的校準回撥不會超過128ms
            // now = plusMillis(previous, HOUSEKEEPING_PERIOD_MS) + 100ms
            if (plusMillis(now, 128) < plusMillis(previous, HOUSEKEEPING_PERIOD_MS)) {
               LOGGER.warn("{} - Retrograde clock change detected (housekeeper delta={}), soft-evicting connections from pool.",
                           poolName, elapsedDisplayString(previous, now));
               previous = now;
               softEvictConnections();
               return;
            }
            else if (now > plusMillis(previous, (3 * HOUSEKEEPING_PERIOD_MS) / 2)) {
               // No point evicting for forward clock motion, this merely accelerates connection retirement anyway
               LOGGER.warn("{} - Thread starvation or clock leap detected (housekeeper delta={}).", poolName, elapsedDisplayString(previous, now));
            }
            previous = now;
            String afterPrefix = "Pool ";
            // 回收符合條件的空閒連線:如果最小連線數等於最大連線數,就不會回收
            if (idleTimeout > 0L && config.getMinimumIdle() < config.getMaximumPoolSize()) {
               logPoolState("Before cleanup ");
               afterPrefix = "After cleanup  ";
               final ListnotInUse = connectionBag.values(STATE_NOT_IN_USE);
               int toRemove = notInUse.size() - config.getMinimumIdle();
               for (PoolEntry entry : notInUse) {
                  // 有空閒連線 且 空閒時間達標 且 CAS更改狀態成功
                  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);
         }
      }
   }
5)存活時間處理

Hikari 在建立一個連線例項的時候,就已經為其繫結了一個定時任務用於關閉連線。

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;
            // 此處 maxLifetime 不能超過資料庫最大允許連線時間
            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;
      }
   }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
關閉連線的過程是先將連線例項標記為廢棄,這樣哪怕因為連線正在使用導致關閉失敗,也可以在下次獲取連線時再對其進行關閉。
複製
private boolean softEvictConnection(final PoolEntry poolEntry, final String reason, final boolean owner)
   {
      // 先標記為廢棄、哪怕下面關閉失敗,getConnection 時也會移除
      poolEntry.markEvicted();
      // 使用中的連線不會關閉
      if (owner || connectionBag.reserve(poolEntry)) {
         closeConnection(poolEntry, reason);
         return true;
      }
      return false;
   }
6)連線洩露處理

Hikari 在處理連線洩露時使用到了工廠模式,只需要將連線例項 PoolEntry 傳入工廠,即可提交連線洩露檢測的延時任務。而所謂的連結洩露檢測只是列印 1 次 WARN 日誌。

class ProxyLeakTaskFactory
{
   private ScheduledExecutorService executorService;
   private long leakDetectionThreshold;
   ProxyLeakTaskFactory(final long leakDetectionThreshold, final ScheduledExecutorService executorService)
   {
      this.executorService = executorService;
      this.leakDetectionThreshold = leakDetectionThreshold;
   }
   // 1、傳入連線物件
   ProxyLeakTask schedule(final PoolEntry poolEntry)
   {  // 連線洩露檢測時間等於 0 不生效
      return (leakDetectionThreshold == 0) ? ProxyLeakTask.NO_LEAK : scheduleNewTask(poolEntry);
   }
   void updateLeakDetectionThreshold(final long leakDetectionThreshold)
   {
      this.leakDetectionThreshold = leakDetectionThreshold;
   }
   // 2、提交延時任務
   private ProxyLeakTask scheduleNewTask(PoolEntry poolEntry) {
      ProxyLeakTask task = new ProxyLeakTask(poolEntry);
      task.schedule(executorService, leakDetectionThreshold);
      return task;
   }
}
ProxyLeakTask
class ProxyLeakTask implements Runnable
{
   private static final Logger LOGGER = LoggerFactory.getLogger(ProxyLeakTask.class);
   static final ProxyLeakTask NO_LEAK;
   private ScheduledFuture scheduledFuture;
   private String connectionName;
   private Exception exception;
   private String threadName;
   private boolean isLeaked;
   static
   {
      NO_LEAK = new ProxyLeakTask() {
         @Override
         void schedule(ScheduledExecutorService executorService, long leakDetectionThreshold) {}
         @Override
         public void run() {}
         @Override
         public void cancel() {}
      };
   }
   ProxyLeakTask(final PoolEntry poolEntry)
   {
      this.exception = new Exception("Apparent connection leak detected");
      this.threadName = Thread.currentThread().getName();
      this.connectionName = poolEntry.connection.toString();
   }
   private ProxyLeakTask()
   {
   }
   void schedule(ScheduledExecutorService executorService, long leakDetectionThreshold)
   {
      scheduledFuture = executorService.schedule(this, leakDetectionThreshold, TimeUnit.MILLISECONDS);
   }
   /** {@inheritDoc} */
   @Override
   public void run()
   {
      isLeaked = true;
      final StackTraceElement[] stackTrace = exception.getStackTrace();
      final StackTraceElement[] trace = new StackTraceElement[stackTrace.length - 5];
      System.arraycopy(stackTrace, 5, trace, 0, trace.length);
      // 列印 1 次連線洩露的 WARN 日誌
      exception.setStackTrace(trace);
      LOGGER.warn("Connection leak detection triggered for {} on thread {}, stack trace follows", connectionName, threadName, exception);
   }
   void cancel()
   {
      scheduledFuture.cancel(false);
      if (isLeaked) {
         LOGGER.info("Previously reported leaked connection {} on thread {} was returned to the pool (unleaked)", connectionName, threadName);
      }
   }
}
7)連線池類分析

ConcurrentBag 才是真正的連線池,也是 Hikari “零開銷”的奧秘所在。

簡而言之,Hikari 透過 CopyOnWriteArrayList + State(狀態) + CAS 來避免了上鎖。

CopyOnWriteArrayList 存放真正的連線物件,每個連線物件都有四種狀態:

  • STATE_NOT_IN_USE:空閒
  • STATE_IN_USE:活躍
  • STATE_REMOVED:移除
  • STATE_RESERVED:不可用
  • 比如在獲取連線時,透過呼叫 bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE) 方法解決併發問題。

public class ConcurrentBagimplements AutoCloseable {
   private static final Logger LOGGER = LoggerFactory.getLogger(ConcurrentBag.class);
   // 所有連線:透過CopyOnWriteArrayList + State + cas 來避免了上鎖
   private final CopyOnWriteArrayListsharedList;
   // threadList是否使用弱引用
   private final boolean weakThreadLocals;
   // 歸還的時候快取空閒連線到 ThreadLocal:requite()、borrow()
   private final ThreadLocal<1list> threadList;
   private final IBagStateListener listener;
   // 等待獲取連線的執行緒數:調 borrow() 方法+1,調完-1
   private final AtomicInteger waiters;
   // 連線池關閉標識
   private volatile boolean closed;
   // 佇列大小為0的阻塞佇列:生產者消費者模式
   private final SynchronousQueuehandoffQueue;
   public interface IConcurrentBagEntry {
      int STATE_NOT_IN_USE = 0; // 空閒
      int STATE_IN_USE = 1; // 活躍
      int STATE_REMOVED = -1; // 移除
      int STATE_RESERVED = -2; // 不可用
      boolean compareAndSet(int expectState, int newState);
      void setState(int newState);
      int getState();
   }
   public interface IBagStateListener {
      void addBagItem(int waiting);
   }
   
   public ConcurrentBag(final IBagStateListener listener) {
      this.listener = listener;
      this.weakThreadLocals = useWeakThreadLocals();
      this.handoffQueue = new SynchronousQueue<>(true);
      this.waiters = new AtomicInteger();
      this.sharedList = new CopyOnWriteArrayList<>();
      if (weakThreadLocals) {
         this.threadList = ThreadLocal.withInitial(() -> new ArrayList<>(16));
      } else {
         this.threadList = ThreadLocal.withInitial(() -> new FastList<>(IConcurrentBagEntry.class, 16));
      }
   }
   public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException {
      // Try the thread-local list first
      // 先從 threadLocal 快取中獲取
      final List list = threadList.get();
      for (int i = list.size() - 1; i >= 0; i--) {
         // 從尾部讀取:後快取的優先用,細節!
         final Object entry = list.remove(i);
         @SuppressWarnings("unchecked") final T bagEntry = weakThreadLocals ? ((WeakReference) entry).get() : (T) entry;
         if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
            return bagEntry;
         }
      }
      // Otherwise, scan the shared list ... then poll the handoff queue
      // 如果本地快取獲取不到,從 shardList 連線池中獲取,等待連線數+1
      final int waiting = waiters.incrementAndGet();
      try {
         for (T bagEntry : sharedList) {
            if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
               // If we may have stolen another waiter's connection, request another bag add.
               // 併發情況下,保證能夠及時補充連線
               if (waiting > 1) {
                  listener.addBagItem(waiting - 1);
               }
               return bagEntry;
            }
         }
         // 如果 shardList 連線池中也沒獲得連線,提交新增連線的非同步任務,然後再從 handoffQueue 阻塞獲取。
         listener.addBagItem(waiting);
         timeout = timeUnit.toNanos(timeout);
         do {
            final long start = currentTime();
            final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);
            if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
               return bagEntry;
            }
            timeout -= elapsedNanos(start);
         } while (timeout > 10_000);
         return null;
      } finally {
         // 等待連線數減 1
         waiters.decrementAndGet();
      }
   }
   
   public void requite(final T bagEntry) {
      bagEntry.setState(STATE_NOT_IN_USE);
      // 如果有執行緒正在獲取連結,則優先透過 handoffQueue 阻塞佇列歸還給其他執行緒使用
      for (int i = 0; waiters.get() > 0; i++) {
         if (bagEntry.getState() != STATE_NOT_IN_USE || handoffQueue.offer(bagEntry)) {
            return;
         } else if ((i & 0xff) == 0xff) {
            // 每遍歷 255 個休眠 10 微妙
            parkNanos(MICROSECONDS.toNanos(10));
         } else {
            // 執行緒讓步
            yield();
         }
      }
      // 沒有其它執行緒用,就放入本地快取
      final List threadLocalList = threadList.get();
      threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry);
   }
   public void add(final T bagEntry) {
      if (closed) {
         LOGGER.info("ConcurrentBag has been closed, ignoring add()");
         throw new IllegalStateException("ConcurrentBag has been closed, ignoring add()");
      }
      sharedList.add(bagEntry);
      // spin until a thread takes it or none are waiting
      // 如果有執行緒等待獲取連線,迴圈透過 handoffQueue 提交連線 
      while (waiters.get() > 0 && !handoffQueue.offer(bagEntry)) {
         yield();
      }
   }
   public boolean remove(final T bagEntry) {
      // 使用 CAS 將連線置為 STATE_REMOVED 狀態
      if (!bagEntry.compareAndSet(STATE_IN_USE, STATE_REMOVED) && !bagEntry.compareAndSet(STATE_RESERVED, STATE_REMOVED) && !closed) {
         LOGGER.warn("Attempt to remove an object from the bag that was not borrowed or reserved: {}", bagEntry);
         return false;
      }
      // CAS 成功後再刪除連線
      final boolean removed = sharedList.remove(bagEntry);
      if (!removed && !closed) {
         LOGGER.warn("Attempt to remove an object from the bag that does not exist: {}", bagEntry);
      }
      return removed;
   }
   @Override
   public void close() {
      closed = true;
   }
   public boolean reserve(final T bagEntry) {
      return bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_RESERVED);
   } 
}
4、報警實戰
1)實戰一

報警日誌
先來看一個真實的線上報警:

Caused by: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is java.sql.SQLTransientConnectionException: hikari-pool - Connection is not available, request timed out after 6791ms.
  at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:81)
  at org.mybatis.spring.transaction.SpringManagedTransaction.openConnection(SpringManagedTransaction.java:80)
  at org.mybatis.spring.transaction.SpringManagedTransaction.getConnection(SpringManagedTransaction.java:67)
  at org.apache.ibatis.executor.BaseExecutor.getConnection(BaseExecutor.java:337)
  at org.apache.ibatis.executor.SimpleExecutor.prepareStatement(SimpleExecutor.java:86)
  at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:62)
  at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:325)
  at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156)
  at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109)
  at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:89)
  ... 125 common frames omitted
Caused by: java.sql.SQLTransientConnectionException: hikari-pool-storecenter - Connection is not available, request timed out after 6791ms.
  at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:669)
  at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:183)
  at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:148)
  at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:100)
  at org.springframework.jdbc.datasource.DataSourceUtils.fetchConnection(DataSourceUtils.java:151)
  at org.springframework.jdbc.datasource.DataSourceUtils.doGetConnection(DataSourceUtils.java:115)
  at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:78)
  ... 134 common frames omitted
Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: [DataSource IP:127.0.0.1:3306] No operations allowed after connection closed.
  at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
  at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
  at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
  at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
  at com.mysql.jdbc.Util.handleNewInstance(Util.java:425)
  at com.mysql.jdbc.Util.getInstance(Util.java:408)
  at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:919)
  at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:898)
  at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:887)
  at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:861)
  at com.mysql.jdbc.ConnectionImpl.throwConnectionClosedException(ConnectionImpl.java:1184)
  at com.mysql.jdbc.ConnectionImpl.checkClosed(ConnectionImpl.java:1179)
  at com.mysql.jdbc.ConnectionImpl.setNetworkTimeout(ConnectionImpl.java:5498)
  at com.zaxxer.hikari.pool.PoolBase.setNetworkTimeout(PoolBase.java:541)
  at com.zaxxer.hikari.pool.PoolBase.isConnectionAlive(PoolBase.java:162)
  at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:172)
  ... 139 common frames omitted

思路分析
No operations allowed after connection closed 表示訪問了已經被 MySQL 關閉的連線。

request timed out after 6791ms 包含等待連線超時 connectionTimeout(配置 2 秒) 和測試連線可用 validationTimeout(預設 5 秒) 兩個時間。

boolean isConnectionAlive(final Connection connection)
   {
      try {
         try {
            setNetworkTimeout(connection, validationTimeout);
            // validationTimeout 預設 5 秒,最低 1 秒
            final int validationSeconds = (int) Math.max(1000L, validationTimeout) / 1000;
            // 測試連結是否有效
            if (isUseJdbc4Validation) {
               return connection.isValid(validationSeconds);
            }
            try (Statement statement = connection.createStatement()) {
               if (isNetworkTimeoutSupported != TRUE) {
                  setQueryTimeout(statement, validationSeconds);
               }
               statement.execute(config.getConnectionTestQuery());
            }
         }
         finally {
            setNetworkTimeout(connection, networkTimeout);
            if (isIsolateInternalQueries && !isAutoCommit) {
               connection.rollback();
            }
         }
         return true;
      }
      catch (Exception e) {
         lastConnectionFailure.set(e);
         // 此處列印 WARN 日誌,可以透過 console.log 檢視是否存在 獲取到已被關閉連線 的情況
         LOGGER.warn("{} - Failed to validate connection {} ({})", poolName, connection, e.getMessage());
         return false;
      }
   }

檢視 console.log,存在大量獲取到已關閉連線的情況:

2022-06-15 01:34:20.445 WARN com.zaxxer.hikari.pool.PoolBase          : hikari-pool - Failed to validate connection com.mysql.jdbc.JDBC4Connection@200203c3 ([DataSource IP:127.0.0.1:3306] No operations allowed after connection closed.)

所以推斷報警原因是因為獲取到已經被資料庫關閉的連線。

解決方法
DBA 反饋資料庫的 wait_timeout 是 600 秒,線上配置的 maxLifeTime 是 900 秒,配置有誤,更改為 450 秒。

上線後驗證 console.log 不再持續列印 Failed to validate connection 日誌,並且沒有 No operations allowed after connection closed 報警日誌。

2)實戰二

報警日誌
最佳化上線後,觀察到又發生了幾十條報警,並且只集中在 1 臺機器:

Caused by: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is java.sql.SQLTransientConnectionException: hikari-pool - Connection is not available, request timed out after 2000ms.
  at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:81)
  at org.mybatis.spring.transaction.SpringManagedTransaction.openConnection(SpringManagedTransaction.java:80)
  at org.mybatis.spring.transaction.SpringManagedTransaction.getConnection(SpringManagedTransaction.java:67)
  at org.apache.ibatis.executor.BaseExecutor.getConnection(BaseExecutor.java:337)
  at org.apache.ibatis.executor.SimpleExecutor.prepareStatement(SimpleExecutor.java:86)
  at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:62)
  at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:325)
  at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156)
  at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109)
  at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:89)
  ... 125 common frames omitted
Caused by: java.sql.SQLTransientConnectionException: hikari-pool - Connection is not available, request timed out after 2000ms.
  at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:669)
  at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:183)
  at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:148)
  at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:100)
  at org.springframework.jdbc.datasource.DataSourceUtils.fetchConnection(DataSourceUtils.java:151)
  at org.springframework.jdbc.datasource.DataSourceUtils.doGetConnection(DataSourceUtils.java:115)
  at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:78)
  ... 134 common frames omitted

思路分析
報警日誌中沒有 No operations allowed after connection closed,且耗時為 connectionTimeout,推測是沒有獲取到連線,原因可能有:

機器異常:機器負載過大有可能引起 IO 夯。

連線池被打滿:比如存在慢SQL,或者流量太大支撐不住等,連線數實在不夠用。Hikari 提供 HikariPoolMXBean 介面獲取連線池監控資訊。

連線洩露:開啟連線洩露引數後,可在日誌中檢視。

解決方法
機器異常:遷移機器,觀察後續情況。

連線池被打滿:增加 Hikari 連線池監控日誌,觀察連線池使用情況,進一步再做判斷。比如可以透過一個定時任務,每秒列印連線池相關狀態:

@Slf4j
@Component
public class HikariPoolTask {
    @Resource
    private MapdataSourceMap;
    /**
     * 延時1秒,每隔1秒
     */
    @Scheduled(initialDelay = 1000, fixedDelay = 1000)
    public void run() {
        if (CollUtil.isNotEmpty(dataSourceMap)) {
            for (HikariDataSource dataSource : dataSourceMap.values()) {
                // 連線池名稱
                String poolName = dataSource.getPoolName();
                HikariPoolMXBean hikariPoolMXBean = dataSource.getHikariPoolMXBean();
                // 活躍連線數量
                int activeConnections = hikariPoolMXBean.getActiveConnections();
                // 空閒連線數量
                int idleConnections = hikariPoolMXBean.getIdleConnections();
                // 全部連線數量
                int totalConnections = hikariPoolMXBean.getTotalConnections();
                // 等待連線數量
                int threadsAwaitingConnection = hikariPoolMXBean.getThreadsAwaitingConnection();
                log.info("{} - activeConnections={}, idleConnections={}, totalConnections={}, threadsAwaitingConnection={}",
                        poolName, activeConnections, idleConnections, totalConnections, threadsAwaitingConnection);
            }
        }
    }
}

連線洩露:增加連線洩露檢測引數,比如可以設定 10 秒

leakDetectionThreshold=10000

作者介紹
薛師兄,在某頭部網際網路公司擔任高階研發工程師,熱衷於Java技術棧,對底層原理有獨特的追求。

原文來自:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69955379/viewspace-2908264/,如需轉載,請註明出處,否則將追究法律責任。

相關文章