換上 HikariCP 連接池,太快了!

来源:https://www.cnblogs.com/javastack/archive/2023/09/01/17671037.html
-Advertisement-
Play Games

![](https://img2023.cnblogs.com/other/1218593/202309/1218593-20230901100033869-964667327.png) ## **背景** 在我們平常的編碼中,通常會將一些對象保存起來,這主要考慮的是對象的創建成本。 比如像線程資源 ...


背景

在我們平常的編碼中,通常會將一些對象保存起來,這主要考慮的是對象的創建成本。

比如像線程資源、資料庫連接資源或者 TCP 連接等,這類對象的初始化通常要花費比較長的時間,如果頻繁地申請和銷毀,就會耗費大量的系統資源,造成不必要的性能損失。

並且這些對象都有一個顯著的特征,就是通過輕量級的重置工作,可以迴圈、重覆地使用。

這個時候,我們就可以使用一個虛擬的池子,將這些資源保存起來,當使用的時候,我們就從池子里快速獲取一個即可。

在 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 是對象池的核心類,通過傳入一個對象池的配置和一個對象的工廠,即可快速創建對象池。

public GenericObjectPool(
            final PooledObjectFactory<T> factory,
            final GenericObjectPoolConfig<T> config)

推薦一個開源免費的 Spring Boot 最全教程:

https://github.com/javastacks/spring-boot-best-practice

案例

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();
    //返回包裝對象
    return new 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本身的屬性
private int maxTotal = DEFAULT_MAX_TOTAL;
private int maxIdle = DEFAULT_MAX_IDLE;
private int minIdle = DEFAULT_MIN_IDLE;
// 其父類BaseObjectPoolConfig的屬性
private boolean lifo = DEFAULT_LIFO;
private boolean fairness = DEFAULT_FAIRNESS;
private long maxWaitMillis = DEFAULT_MAX_WAIT_MILLIS;
private long minEvictableIdleTimeMillis = DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS;
private long evictorShutdownTimeoutMillis = DEFAULT_EVICTOR_SHUTDOWN_TIMEOUT_MILLIS;
private long softMinEvictableIdleTimeMillis = DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS;
private int numTestsPerEvictionRun = DEFAULT_NUM_TESTS_PER_EVICTION_RUN;
private EvictionPolicy<T> evictionPolicy = null;
// Only 2.6.0 applications set this
private String evictionPolicyClassName = DEFAULT_EVICTION_POLICY_CLASS_NAME;
private boolean testOnCreate = DEFAULT_TEST_ON_CREATE;
private boolean testOnBorrow = DEFAULT_TEST_ON_BORROW;
private boolean testOnReturn = DEFAULT_TEST_ON_RETURN;
private boolean testWhileIdle = DEFAULT_TEST_WHILE_IDLE;
private long timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
private boolean blockWhenExhausted = DEFAULT_BLOCK_WHEN_EXHAUSTED;

參數很多,要想瞭解參數的意義,我們首先來看一下一個池化對象在整個池子中的生命周期。

如下圖所示,池子的操作主要有兩個:一個是業務線程,一個是檢測線程。

對象池在進行初始化時,要指定三個主要的參數:

  • maxTotal 對象池中管理的對象上限
  • maxIdle 最大空閑數
  • minIdle 最小空閑數

其中 maxTotal 和業務線程有關,當業務線程想要獲取對象時,會首先檢測是否有空閑的對象。

如果有,則返回一個;否則進入創建邏輯。此時,如果池中個數已經達到了最大值,就會創建失敗,返回空對象。

對象在獲取的時候,有一個非常重要的參數,那就是最大等待時間(maxWaitMillis),這個參數對應用方的性能影響是比較大的。該參數預設為 -1,表示永不超時,直到有對象空閑。

如下圖,如果對象創建非常緩慢或者使用非常繁忙,業務線程會持續阻塞 (blockWhenExhausted 預設為 true),進而導致正常服務也不能運行。

面試題

一般面試官會問:你會把超時參數設置成多大呢?我一般都會把最大等待時間,設置成介面可以忍受的最大延遲。

最新 Java 面試題最全整理:https://www.javastack.cn/mst/

比如,一個正常服務響應時間 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 class JedisPoolVSJedisBenchmark {
   JedisPool pool = new JedisPool("localhost", 6379);

   @Benchmark
   public void testPool() {
       Jedis jedis = pool.getResource();
       jedis.set("a", UUID.randomUUID().toString());
       jedis.close();
   }

   @Benchmark
   public void testJedis() {
       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 對性能的一些優化操作,是非常值得我們借鑒的,在之後的博客中,我們將詳細分析幾個優化場景。

資料庫連接池同樣面臨一個最大值(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 是我重點研究的類庫之一,我也建議你加入自己的任務清單中。

總體來說,當你遇到下麵的場景,就可以考慮使用池化來增加系統性能:

  • 對象的創建或者銷毀,需要耗費較多的系統資源
  • 對象的創建或者銷毀,耗時長,需要繁雜的操作和較長時間的等待
  • 對象創建後,通過一些狀態重置,可被反覆使用

將對象池化之後,只是開啟了第一步優化。要想達到最優性能,就不得不調整池的一些關鍵參數,合理的池大小加上合理的超時時間,就可以讓池發揮更大的價值。和緩存的命中率類似,對池的監控也是非常重要的。

如下圖,可以看到資料庫連接池連接數長時間保持在高位不釋放,同時等待的線程數急劇增加,這就能幫我們快速定位到資料庫的事務問題。

平常的編碼中,有很多類似的場景。比如 Http 連接池,Okhttp 和 Httpclient 就都提供了連接池的概念,你可以類比著去分析一下,關註點也是在連接大小和超時時間上。

在底層的中間件,比如 RPC,也通常使用連接池技術加速資源獲取,比如 Dubbo 連接池、 Feign 切換成 httppclient 的實現等技術。

你會發現,在不同資源層面的池化設計也是類似的。比如線程池,通過隊列對任務進行了二層緩衝,提供了多樣的拒絕策略等,線程池我們將在後續的文章中進行介紹。

線程池的這些特性,你同樣可以借鑒到連接池技術中,用來緩解請求溢出,創建一些溢出策略。

現實情況中,我們也會這麼做。那麼具體怎麼做?有哪些做法?這部分內容就留給大家思考了。

版權聲明:本文為CSDN博主「農民工老王」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。

原文鏈接:https://blog.csdn.net/monarch91/article/details/123867269

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2022最新版)

2.勁爆!Java 協程要來了。。。

3.Spring Boot 2.x 教程,太全了!

4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!

5.《Java開發手冊(嵩山版)》最新發佈,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 切片與數組類似,但更強大和靈活。與數組一樣,切片也用於在單個變數中存儲相同類型的多個值。然而,與數組不同的是,切片的長度可以根據需要增長和縮小。在 Go 中,有幾種創建切片的方法: 1. 使用`[]datatype{values}`格式 2. 從數組創建切片 3. 使用 `make()`函數 使用 ...
  • # 代理模式 目標類和代理類,不是直接調用目標類對象,而是通過調用代理類的對象的方法,代理類來幫我們訪問目標類對象,這樣我們就可以在代理類上添加更多需要的擴展功能,而目標類不用改動,只用實現自身的主要功能。 代理類是為了擴展目標類的功能,代理類和目標類的產出結果應該相同,所以為了確保代理類和目標類的 ...
  • 本文翻譯自國外論壇 medium,原文地址:https://medium.com/@fullstacktips/best-practices-for-memory-management-in-java-17084c4a7eec 記憶體管理是編程的一個基本領域之一,尤其是在 Java 開發中。當不再需要 ...
  • ## pprof簡介 `pprof`是Go語言的一個性能分析庫,它可以幫助開發者找出程式中的性能瓶頸。`pprof`提供了CPU分析、記憶體分析、阻塞分析等多種性能分析功能。 以下是`pprof`的主要特性: 1. **CPU分析**:`pprof`可以記錄程式在CPU上的運行時間,並將這些數據以火焰 ...
  • [TOC]([Qt開發探幽(二)]淺談關於元對象,巨集和Q_ENUM) # [Qt開發探幽(二)]淺談關於元對象,巨集和Q_ENUM ## 前言 最近在開發的時候,我自己寫了一套虛函數。這也是我第一次寫這麼大一個框架,遇到了一些有點莫名其妙的問題(也不能算莫名奇妙,只能說有點玩不明白),詳情可以見 [[ ...
  • acwing學習筆記,記錄容易忘記的知識點和難題。快速排序、歸併排序、整數二分、浮點數二分、高精度運算、一維首碼和、二維首碼和、一維差分、二維差分、雙指針演算法、位運算、整數離散化、區間合併 ...
  • 使用`Matplotlib`對分析結果可視化時,比較各類分析結果是常見的場景。在這類場景之下,將多個分析結果繪製在一張圖上,可以幫助用戶方便地組合和分析多個數據集,提高數據可視化的效率和準確性。 本篇介紹`Matplotlib`繪製子圖的常用方式和技巧。 # 1. 添加子圖的方式 添加子圖主要有兩種 ...
  • 在筆者前幾篇文章中我們一直在探討如何利用`Metasploit`這個滲透工具生成`ShellCode`以及如何將ShellCode註入到特定進程內,本章我們將自己實現一個正向`ShellCode`Shell,當進程被註入後,則我們可以通過利用NC等工具連接到被註入進程內,並以對方的許可權及身份執行命令... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...