二、HikariCP源碼分析之獲取連接流程二

歡迎訪問(wèn)我的博客,同步更新: 楓山別院

HikariPool的getConnection()方法

在上一篇《HikariCP獲取連接流程源碼分析一》中,我們分析了HikariDataSource的getConnection()方法,而這個(gè)方法,其實(shí)詳細(xì)的實(shí)現(xiàn)細(xì)節(jié)都是在HikariPool的getConnection()方法中,我們來(lái)分析下HikariPool的getConnection()方法。

代碼如下:

public final Connection getConnection() throws SQLException {
      return getConnection(connectionTimeout);
   }

這里又調(diào)用了一個(gè)有參的getConnection()方法,但是我們并沒(méi)有傳參數(shù)connectionTimeout,這個(gè)是哪里來(lái)的呢?這個(gè)其實(shí)就是用戶在初始化連接池的時(shí)候設(shè)置的參數(shù)connectionTimeout,它表示獲取連接的超時(shí)時(shí)間,不配置的話默認(rèn)值 30秒。我們繼續(xù)看下getConnection(connectionTimeout);的實(shí)現(xiàn):

public final Connection getConnection(final long hardTimeout) throws SQLException {
      //①
      //獲取連接的時(shí)候申請(qǐng)令牌, 主要是為了連接池掛起的時(shí)候, 控制用戶不能獲取連接
      //當(dāng)連接池掛起的時(shí)候, Semaphore的 10000 個(gè)令牌都會(huì)被占用, 此處就會(huì)一直阻塞線程等待令牌
      suspendResumeLock.acquire();
      //記錄獲取連接的開(kāi)始時(shí)間, 用于超時(shí)判斷
      final long startTime = clockSource.currentTime();

      try {
         long timeout = hardTimeout;
         do {
            //②
            //從連接池獲取連接, 超時(shí)時(shí)間timeout
            final PoolEntry poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
            //borrow方法在超時(shí)的時(shí)候才會(huì)返回 null
            if (poolEntry == null) {
               break; // We timed out... break and throw exception
            }

            final long now = clockSource.currentTime();
            //③
            //獲取連接的時(shí)候, 判斷連接是否已經(jīng)被標(biāo)記移除
            if (poolEntry.isMarkedEvicted() || (clockSource.elapsedMillis(poolEntry.lastAccessed, now) > ALIVE_BYPASS_WINDOW_MS && !isConnectionAlive(poolEntry.connection))) {
               //如果連接超出maxLifetime, 或者連接測(cè)試不通過(guò), 就關(guān)閉連接
               closeConnection(poolEntry, "(connection is evicted or dead)"); // Throw away the dead connection (passed max age or failed alive test)
               //剩余超時(shí)時(shí)間
               timeout = hardTimeout - clockSource.elapsedMillis(startTime);
            } else {
               //④
               //記錄連接借用
               metricsTracker.recordBorrowStats(poolEntry, startTime);
               //創(chuàng)建ProxyConnection, ProxyConnection是Connection的包裝, 同時(shí)也創(chuàng)建一個(gè)泄露檢測(cè)的定時(shí)任務(wù)
               return poolEntry.createProxyConnection(leakTask.schedule(poolEntry), now);
            }
         } while (timeout > 0L);
      } catch (InterruptedException e) {
         throw new SQLException(poolName + " - Interrupted during connection acquisition", e);
      } finally {
         //釋放鎖
         suspendResumeLock.release();
      }

      //⑤
      //獲取連接超時(shí)才會(huì)執(zhí)行下面的代碼
      logPoolState("Timeout failure ");
      metricsTracker.recordConnectionTimeout();

      String sqlState = null;
      final Throwable originalException = getLastConnectionFailure();
      if (originalException instanceof SQLException) {
         sqlState = ((SQLException) originalException).getSQLState();
      }
      final SQLException connectionException = new SQLTransientConnectionException(poolName + " - Connection is not available, request timed out after " + clockSource.elapsedMillis(startTime) + "ms.", sqlState, originalException);
      if (originalException instanceof SQLException) {
         connectionException.setNextException((SQLException) originalException);
      }
      throw connectionException;
   }

①Semaphore

suspendResumeLock.acquire();
//記錄獲取連接的開(kāi)始時(shí)間, 用于超時(shí)判斷
final long startTime = clockSource.currentTime();

getConnection的第一步,首先是獲取令牌。我們從變量的名字suspendResumeLock來(lái)看,可能跟掛起(suspend)有關(guān),那么掛起什么東西?如果之前大家有讀過(guò) HikariCP 的文檔,或者使用過(guò)HikariCP的掛起功能,那么你肯定已經(jīng)猜到了,這個(gè)是跟掛起整個(gè)連接池有關(guān)。

  • 掛起HikariCP

HikariCP的掛起功能,其實(shí)就是暫停用戶獲取連接,也就是說(shuō),掛起整個(gè)連接池之后,如果有線程要從連接池獲取連接,那么會(huì)一直阻塞,直到連接池被恢復(fù)。

掛起有什么用?

作者 brett 提到掛起的使用方法:

  1. 掛起連接池

  2. 更改數(shù)據(jù)庫(kù)連接池配置,或者更改 DNS 配置(指向新的主服務(wù)器)

  3. 軟驅(qū)逐連接池中現(xiàn)有的連接

  4. 恢復(fù)連接池

HikariCP可以在運(yùn)行期通過(guò) JMX修改一些配置的(并不是所有的配置), 有:connectionTimeout(獲取連接的超時(shí)時(shí)間),leakDetectionThreshold(連接泄露檢測(cè)時(shí)間),maxPoolSize(連接池最大連接數(shù)),minIdle(最小空閑連接數(shù)),maxLifetime(連接最大存活時(shí)間),idleTimeout(連接最大空閑時(shí)間),共 7 項(xiàng)。

比如我掛起了連接池,然后修改了maxLifetime,那么連接池中現(xiàn)有的連接還是之前的配置,我就要將所有的連接都從連接池中驅(qū)逐出去,然后恢復(fù)連接池,這時(shí)候連接池就會(huì)使用新的配置創(chuàng)建新的連接。

除此之外,還可以使用連接池掛起時(shí),線程一直阻塞無(wú)法獲取到連接這個(gè)特性,來(lái)模擬數(shù)據(jù)庫(kù)連接故障,來(lái)測(cè)試應(yīng)用。

  • 怎么實(shí)現(xiàn)的

OK,我們知道了這一句代碼的目的主要是掛起連接池時(shí),阻止用戶獲取連接的。那么是怎么實(shí)現(xiàn)的呢?

其實(shí),suspendResumeLock的類(lèi)是com.zaxxer.hikari.util.SuspendResumeLock,它的內(nèi)部是用Semaphore實(shí)現(xiàn)的。Semaphore是 java 的concurrent包下的 并發(fā)工具類(lèi),它用給線程發(fā)放令牌的方式,控制線程的并發(fā)數(shù)量。

舉個(gè)場(chǎng)景例子,假如是秒殺:我們知道服務(wù)器的最大并發(fā)處理能力是同時(shí)處理 1000 個(gè)請(qǐng)求,超過(guò) 1000 個(gè)請(qǐng)求服務(wù)器可能會(huì)宕機(jī),在不擴(kuò)容的情況下,盡量保證服務(wù)可用。這個(gè)時(shí)候,我們就要控制用戶的請(qǐng)求數(shù)量不能超過(guò) 1000對(duì)吧。

這時(shí)候我們可以用Semaphore實(shí)現(xiàn),Semaphore類(lèi)似一個(gè)令牌桶,桶里可以放指定數(shù)量的令牌,在并發(fā)的時(shí)候,每個(gè)線程從桶里拿一個(gè)(也可以是多個(gè))令牌,拿到令牌的才可以繼續(xù)執(zhí)行,拿不到令牌的就等著(根據(jù)策略不同,也可能是拋異常等),直到有其他線程釋放令牌之后,得到令牌繼續(xù)執(zhí)行。

上面的場(chǎng)景,我們可以使用Semaphore初始化 1000 個(gè)令牌,每個(gè)線程拿一個(gè)令牌,這樣我們就可以控制同時(shí)處理的請(qǐng)求數(shù)量不超過(guò) 1000了吧。

同樣的道理,我們看下掛起連接池的方法:

public void suspend() {
      //MAX_PERMITS = 10000
      acquisitionSemaphore.acquireUninterruptibly(MAX_PERMITS);
   }

HikariCP 在這里初始化了 1萬(wàn)個(gè)令牌,如果用戶調(diào)用了suspend()掛起連接池,其實(shí)就是調(diào)用了Semaphore一次獲取 1 萬(wàn)個(gè)令牌,這樣其他線程就沒(méi)有令牌可以拿了,只能一直等,直到用戶恢復(fù)線程池,釋放這 1 萬(wàn)個(gè)令牌到桶里。

需要注意的是,要使用掛起連接池的功能,必須配置isAllowPoolSuspension=true,否則使用掛起功能會(huì)報(bào)錯(cuò)。

我們提到isAllowPoolSuspension其實(shí)是還要說(shuō)一下suspendResumeLock的一個(gè)優(yōu)化點(diǎn)。

在初始化連接池的時(shí)候,這個(gè)suspendResumeLock根據(jù)你是否開(kāi)啟了掛起功能,會(huì)有不同的實(shí)現(xiàn),this.suspendResumeLock = config.isAllowPoolSuspension() ? new SuspendResumeLock() : SuspendResumeLock.FAUX_LOCK;

假如沒(méi)有啟用掛起功能,那么suspendResumeLock是一個(gè)FAUX_LOCK。

FAUX_LOCK是什么呢?看代碼:

public static final SuspendResumeLock FAUX_LOCK = new SuspendResumeLock(false) {
      @Override
      public void acquire() {}
      @Override
      public void release() {}
      @Override
      public void suspend() {}
      @Override
      public void resume() {}
   };

你沒(méi)有看錯(cuò),就是一個(gè)空實(shí)現(xiàn),方法里什么都沒(méi)有。

這么做有什么好處?其實(shí)是,當(dāng)掛起功能沒(méi)有開(kāi)啟的時(shí)候, 它會(huì)提供一個(gè)空實(shí)現(xiàn), 希望 JIT 能將之優(yōu)化掉。也就是說(shuō),每次申請(qǐng)令牌其實(shí)是調(diào)用空方法,什么都不干,代碼在運(yùn)行多次之后,JIT 有可能會(huì)把它優(yōu)化掉,根本就不調(diào)用了。這樣,我們每次獲取連接的時(shí)候,會(huì)節(jié)省申請(qǐng)令牌的額外開(kāi)銷(xiāo),提高性能。

最快的一般不是浪得虛名,肯定都有值得我們學(xué)習(xí)的地方,除了最快的男人......你學(xué)會(huì)了沒(méi)有?

我們繼續(xù)分析。

clockSource是一個(gè)時(shí)間的工具類(lèi),用于獲取當(dāng)前時(shí)間,計(jì)算時(shí)間差等等。此處記錄了當(dāng)前時(shí)間,用于后面時(shí)間差計(jì)算,判斷獲取連接是否超時(shí)用的。

②獲取連接

//②
//從連接池獲取連接, 超時(shí)時(shí)間timeout
final PoolEntry poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
//borrow方法在超時(shí)的時(shí)候才會(huì)返回 null
if (poolEntry == null) {
  break; // We timed out... break and throw exception
}

此處的代碼,我們可以看到,從connectionBag中獲取了一個(gè)poolEntry對(duì)象。poolEntry其實(shí)是對(duì)數(shù)據(jù)庫(kù)連接的一個(gè)包裝類(lèi),connectionBag才是 HikariCP中實(shí)際保存數(shù)據(jù)庫(kù)連接的容器,里面是一個(gè)CopyOnWriteArrayList。由于connectionBag非常重要,我們要在后面單獨(dú)分析,此處不深入進(jìn)去了。

但是從connectionBag獲取連接的時(shí)候,我可以看到傳了一個(gè)參數(shù)timeout,這個(gè)timeout就是我們配置的connectionTimeout,獲取連接的超時(shí)時(shí)間,如果在指定的timeout時(shí)間內(nèi),沒(méi)有返回一個(gè)連接,那么就返回一個(gè) null。

此時(shí)已經(jīng)超時(shí)了,所以下面的判斷就是跳出循環(huán),不在嘗試獲取連接了。

由于放在一篇文章里太長(zhǎng),未盡事宜,下篇繼續(xù)!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容