大家好,我是鵬磊。
大家好,現在介紹一款非常強大,高效,並且號稱「史上最快連線池」。由此可見他是有多受人喜歡,並且
在SpringBoot2.0之後,采用的預設資料庫連線池就是Hikari 。
我們知道的連線池有C3P0,DBCP,它們都比較成熟穩定,但效能不是十分好。所以有了BoneCP這個連線池,它是一個高速、免費、開源的JAVA連線池,它的效能幾乎是C3P0、DBCP的25倍,十分強悍
在我們平常的編碼中,通常會將一些物件保存起來,這主要考慮的是物件的建立成本。
比如像執行緒資源、資料庫連線資源或者 TCP 連線等,這類物件的初始化通常要花費比較長的時間,如果頻繁地申請和銷毀,就會耗費大量的系統資源,造成不必要的效能損失。
並且這些物件都有一個顯著的特征,就是透過輕量級的重設工作,可以迴圈、重復地使用。
這個時候,我們就可以使用一個虛擬的池子,將這些資源保存起來,當使用的時候,我們就從池子裏快速獲取一個即可。
如果你近期準備面試跳槽,建議在ddkk.com線上刷題,涵蓋 一萬+ 道 Java 面試題,幾乎覆蓋了所有主流技術面試題,還有市面上最全的技術五百套,精品系列教程,免費提供。
在Java 中,池化技術套用非常廣泛,常見的就有資料庫連線池、執行緒池等,本文主講連線池,執行緒池我們將在後續的部落格中進行介紹。
公用池化包 Commons Pool 2
我們首先來看一下 Java 中公用的池化包 Commons Pool 2,來了解一下物件池的一般結構。
根據我們的業務需求,使用這套 API 能夠很容易實作物件的池化管理。
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.11.1</version>
</dependency>
GenericObjectPool 是物件池的核心類,透過傳入一個物件池的配置和一個物件的工廠,即可快速建立物件池。
publicGenericObjectPool(
final PooledObjectFactory<T> factory,
final GenericObjectPoolConfig<T> config)
案例
Redis 的常用客戶端 Jedis,就是使用 Commons Pool 管理連線池的,可以說是一個最佳實踐。下圖是 Jedis 使用工廠建立物件的主要程式碼塊。
物件工廠類最主要的方法就是makeObject,它的返回值是 PooledObject 型別,可以將物件使用 new DefaultPooledObject<>(obj) 進行簡單包裝返回。
redis.clients.jedis.JedisFactory,使用工廠建立物件。
@Override
public PooledObject<Jedis> makeObject()throws Exception {
Jedis jedis = null;
try {
jedis = new Jedis(jedisSocketFactory, clientConfig);
//主要的耗時操作
jedis.connect();
//返回包裝物件
returnnew DefaultPooledObject<>(jedis);
} catch (JedisException je) {
if (jedis != null) {
try {
jedis.quit();
} catch (RuntimeException e) {
logger.warn("Error while QUIT", e);
}
try {
jedis.close();
} catch (RuntimeException e) {
logger.warn("Error while close", e);
}
}
throw je;
}
}
我們再來介紹一下物件的生成過程,如下圖,物件在進行獲取時,將首先嘗試從物件池裏拿出一個,如果物件池中沒有空閑的物件,就使用工廠類提供的方法,生成一個新的。
public T borrowObject(final Duration borrowMaxWaitDuration)throws Exception {
//此處省略若幹行
while (p == null) {
create = false;
//首先嘗試從池子中獲取。
p = idleObjects.pollFirst();
// 池子裏獲取不到,才呼叫工廠內生成新例項
if (p == null) {
p = create();
if (p != null) {
create = true;
}
}
//此處省略若幹行
}
//此處省略若幹行
}
那物件是存在什麽地方的呢?這個儲存的職責,就是由一個叫作 LinkedBlockingDeque 的結構來承擔的,它是一個雙向的佇列。
接下來看一下 GenericObjectPoolConfig 的主要內容:
// GenericObjectPoolConfig本身的內容
privateint maxTotal = DEFAULT_MAX_TOTAL;
privateint maxIdle = DEFAULT_MAX_IDLE;
privateint minIdle = DEFAULT_MIN_IDLE;
// 其父類BaseObjectPoolConfig的內容
privateboolean lifo = DEFAULT_LIFO;
privateboolean fairness = DEFAULT_FAIRNESS;
privatelong maxWaitMillis = DEFAULT_MAX_WAIT_MILLIS;
privatelong minEvictableIdleTimeMillis = DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS;
privatelong evictorShutdownTimeoutMillis = DEFAULT_EVICTOR_SHUTDOWN_TIMEOUT_MILLIS;
privatelong softMinEvictableIdleTimeMillis = DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS;
privateint numTestsPerEvictionRun = DEFAULT_NUM_TESTS_PER_EVICTION_RUN;
private EvictionPolicy<T> evictionPolicy = null;
// Only 2.6.0 applications set this
private String evictionPolicy className = DEFAULT_EVICTION_POLICY_ class_NAME;
privateboolean testOnCreate = DEFAULT_TEST_ON_CREATE;
privateboolean testOnBorrow = DEFAULT_TEST_ON_BORROW;
privateboolean testOnReturn = DEFAULT_TEST_ON_RETURN;
privateboolean testWhileIdle = DEFAULT_TEST_WHILE_IDLE;
privatelong timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
privateboolean blockWhenExhausted = DEFAULT_BLOCK_WHEN_EXHAUSTED;
參數很多,要想了解參數的意義,我們首先來看一下一個池化物件在整個池子中的生命周期。
如下圖所示,池子的操作主要有兩個:一個是業務執行緒,一個是檢測執行緒。
物件池在進行初始化時,要指定三個主要的參數:
maxTotal 物件池中管理的物件上限
maxIdle 最大空閑數
minIdle 最小空閑數
其中maxTotal 和業務執行緒有關,當業務執行緒想要獲取物件時,會首先檢測是否有空閑的物件。
如果有,則返回一個;否則進入建立邏輯。此時,如果池中個數已經達到了最大值,就會建立失敗,返回空物件。
物件在獲取的時候,有一個非常重要的參數,那就是最大等待時間(maxWaitMillis),這個參數對套用方的效能影響是比較大的。該參數預設為 -1,表示永不超時,直到有物件空閑。
如下圖,如果物件建立非常緩慢或者使用非常繁忙,業務執行緒會持續阻塞 (blockWhenExhausted 預設為 true),進而導致正常服務也不能執行。
面試題
一般面試官會問:你會把超時參數設定成多大呢?我一般都會把最大等待時間,設定成介面可以忍受的最大延遲。
比如,一個正常服務響應時間 10ms 左右,達到 1 秒鐘就會感覺到卡頓,那麽這個參數設定成 500~1000ms 都是可以的。
超時之後,會丟擲 NoSuchElementException 異常,請求會快速失敗,不會影響其他業務執行緒,這種 Fail Fast 的思想,在互聯網套用非常廣泛。
帶有evcit 字樣的參數,主要是處理物件逐出的。池化物件除了初始化和銷毀的時候比較昂貴,在執行時也會占用系統資源。
比如,連線池會占用多條連線,執行緒池會增加排程開銷等。業務在突發流量下,會申請到超出正常情況的物件資源,放在池子中。等這些物件不再被使用,我們就需要把它清理掉。
超出minEvictableIdleTimeMillis 參數指定值的物件,就會被強制回收掉,這個值預設是 30 分鐘;softMinEvictableIdleTimeMillis 參數類似,但它只有在當前物件數量大於 minIdle 的時候才會執行移除,所以前者的動作要更暴力一些。
還有4 個 test 參數:testOnCreate、testOnBorrow、testOnReturn、testWhileIdle,分別指定了在建立、獲取、歸還、空閑檢測的時候,是否對池化物件進行有效性檢測。
開啟這些檢測,能保證資源的有效性,但它會耗費效能,所以預設為 false。
生產環境上,建議只將 testWhileIdle 設定為 true,並透過調整空閑檢測時間間隔(timeBetweenEvictionRunsMillis),比如 1 分鐘,來保證資源的可用性,同時也保證效率。
JMH 測試
使用連線池和不使用連線池,它們之間的效能差距到底有多大呢?
下面是一個簡單的 JMH 測試例子(見倉庫),進行一個簡單的 set 操作,為 redis 的 key 設定一個隨機值。
@Fork(2)
@State(Scope.Benchmark)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
@BenchmarkMode(Mode.Throughput)
public classJedisPoolVSJedisBenchmark{
JedisPool pool = new JedisPool("localhost", 6379);
@Benchmark
publicvoidtestPool(){
Jedis jedis = pool.getResource();
jedis.set("a", UUID.randomUUID().toString());
jedis.close();
}
@Benchmark
publicvoidtestJedis(){
Jedis jedis = new Jedis("localhost", 6379);
jedis.set("a", UUID.randomUUID().toString());
jedis.close();
}
//此處省略若幹行
}
將測試結果使用 meta-chart 作圖,展示結果如下圖所示,可以看到使用了連線池的方式,它的吞吐量是未使用連線池方式的 5 倍!
資料庫連線池 HikariCP
HikariCP 源於日語「光る」,光的意思,寓意軟體工作速度和光速一樣快,它是 SpringBoot 中預設的資料庫連線池。
資料庫是我們工作中經常使用到的元件,針對資料庫設計的客戶端連線池是非常多的,它的設計原理與我們在本文開頭提到的基本一致,可以有效地減少資料庫連線建立、銷毀的資源消耗。
同是連線池,它們的效能也是有差別的,下圖是 HikariCP 官方的一張測試圖,可以看到它優異的效能,官方的 JMH 測試程式碼見 Github。
一般面試題是這麽問的:HikariCP 為什麽快呢?
主要有三個方面:
它使用 FastList 替代 ArrayList,透過初始化的預設值,減少了越界檢查的操作
最佳化並精簡了字節碼,透過使用 Javassist,減少了動態代理的效能損耗,比如使用 invokestatic 指令代替 invokevirtual 指令
實作了無鎖的 ConcurrentBag,減少了並行場景下的鎖競爭
HikariCP 對效能的一些最佳化操作,是非常值得我們借鑒的,在之後的部落格中,我們將詳細分析幾個最佳化場景。
如果你近期準備面試跳槽,建議在ddkk.com線上刷題,涵蓋 一萬+ 道 Java 面試題,幾乎覆蓋了所有主流技術面試題,還有市面上最全的技術五百套,精品系列教程,免費提供。
資料庫連線池同樣面臨一個最大值(maximumPoolSize)和最小值(minimumIdle)的問題。這裏同樣有一個非常高頻的面試題:你平常會把連線池設定成多大呢?
很多同學認為,連線池的大小設定得越大越好,有的同學甚至把這個值設定成 1000 以上,這是一種誤解。
根據經驗,資料庫連線,只需要 20~50 個就夠用了。具體的大小,要根據業務內容進行調整,但大得離譜肯定是不合適的。
HikariCP 官方是不推薦設定 minimumIdle 這個值的,它將被預設設定成和 maximumPoolSize 一樣的大小。如果你的資料庫Server端連線資源空閑較大,不妨也可以去掉連線池的動態調整功能。
另外,根據資料庫查詢和事務型別,一個套用中是可以配置多個資料庫連線池的,這個最佳化技巧很少有人知道,在此簡要描述一下。
業務型別通常有兩種:一種需要快速的響應時間,把數據盡快返回給使用者;另外一種是可以在後台慢慢執行,耗時比較長,對時效性要求不高。
如果這兩種業務型別,共用一個資料庫連線池,就容易發生資源爭搶,進而影響介面響應速度。
雖然微服務能夠解決這種情況,但大多數服務是沒有這種條件的,這時就可以對連線池進行拆分。
如圖,在同一個業務中,根據業務的內容,我們分了兩個連線池,就是來處理這種情況的。
HikariCP 還提到了另外一個知識點,在 JDBC4 的協定中,透過 Connection.isValid() 就可以檢測連線的有效性。
這樣,我們就不用設定一大堆的 test 參數了,HikariCP 也沒有提供這樣的參數。
結果緩存池
到了這裏你可能會發現池(Pool)與緩存(Cache)有許多相似之處。
它們之間的一個共同點,就是將物件加工後,儲存在相對高速的區域。我習慣性將緩存看作是數據物件,而把池中的物件看作是執行物件。緩存中的數據有一個命中率問題,而池中的物件一般都是對等的。
考慮下面一個場景,jsp 提供了網頁的動態功能,它可以在執行後,編譯成 class 檔,加快執行速度;再或者,一些媒體平台,會將熱門文章,定時轉化成靜態的 html 頁面,僅靠 nginx 的負載均衡即可應對高並行請求(動靜分離)。
這些時候,你很難說清楚,這是針對緩存的最佳化,還是針對物件進行了池化,它們在本質上只是保存了某個執行步驟的結果,使得下次存取時不需要從頭再來。
我通常把這種技術叫作結果緩存池(Result Cache Pool),屬於多種最佳化手段的綜合。
小結
下面我來簡單總結一下本文的內容積比重點:我們從 Java 中最通用的公用池化包 Commons Pool 2 說起,介紹了它的一些實作細節,並對一些重要參數的套用做了講解。
Jedis 就是在 Commons Pool 2 的基礎上封裝的,透過 JMH 測試,我們發現物件池化之後,有了接近 5 倍的效能提升。
接下來介紹了資料庫連線池中速度很快的 HikariCP ,它在池化技術之上,又透過編碼技巧進行了進一步的效能提升,HikariCP 是我重點研究的類別庫之一,我也建議你加入自己的任務清單中。
如果你近期準備面試跳槽,建議在ddkk.com線上刷題,涵蓋 一萬+ 道 Java 面試題,幾乎覆蓋了所有主流技術面試題,還有市面上最全的技術五百套,精品系列教程,免費提供。
總體來說,當你遇到下面的場景,就可以考慮使用池化來增加系統效能:
物件的建立或者銷毀,需要耗費較多的系統資源
物件的建立或者銷毀,耗時長,需要繁雜的操作和較長時間的等待
物件建立後,透過一些狀態重設,可被反復使用
將物件池化之後,只是開啟了第一步最佳化。要想達到最優效能,就不得不調整池的一些關鍵參數,合理的池大小加上合理的超時時間,就可以讓池發揮更大的價值。和緩存的命中率類似,對池的監控也是非常重要的。
如下圖,可以看到資料庫連線池連線數長時間保持在高位不釋放,同時等待的執行緒數急劇增加,這就能幫我們快速定位到資料庫的事務問題。
平常的編碼中,有很多類似的場景。比如 Http 連線池,Okhttp 和 Httpclient 就都提供了連線池的概念,你可以類比著去分析一下,關註點也是在連線大小和超時時間上。
在底層的中介軟體,比如 RPC,也通常使用連線池技術加速資源獲取,比如 Dubbo 連線池、 Feign 切換成 httppclient 的實作等技術。
你會發現,在不同資源層面的池化設計也是類似的。比如執行緒池,透過佇列對任務進行了二層緩沖,提供了多樣的拒絕策略等,執行緒池我們將在後續的文章中進行介紹。
執行緒池的這些特性,你同樣可以借鑒到連線池技術中,用來緩解請求溢位,建立一些溢位策略。
現實情況中,我們也會這麽做。那麽具體怎麽做?有哪些做法?這部份內容就留給大家思考了。
🔥 磊哥私藏精品 熱門推薦 🔥