Spring Boot2.5 集成數據庫連接池 HikariCP


摘要:淺談連接池基本概念和工作原理、常見數據庫連接池性能對比、HiKariCP速度為什么快和常見屬性對比。最后給出一個Spring Boot整合HiKariCP的入門案例。

§工程環境

  • JDK:1.8.0_231
  • maven:3.6.1
  • Apache Tomcat:9.0.46
  • Spring Boot: 2.5.0
  • mysql-connector-java:8.0.25
  • mysql:8.0.25
  • HikariCP:4.0.3

§數據庫連接池介紹

  一個普通的java程序,要查詢數據庫的數據,基本流程是這樣的:

  可以看到:進行一次查詢,要進行很多次網絡交互,這樣的缺點是:

  1. 網絡IO多

  2. 響應時間長,導致QPS降低

  3. 頻繁創建連接和關閉連接,浪費數據庫資源,影響服務器性能

  因為TCP連接的創建開支十分昂貴,並且數據庫所能承載的TCP並發連接數也有限制,針對這種場景,數據庫連接池應運而生。數據庫連接池是用於創建、管理和釋放數據庫連接的緩沖池技術,緩沖池中的連接可以被任何需要它們的線程使用。當一個線程需要用JDBC對一個數據庫操作時,將從池中請求一個連接;當這個連接使用完畢后,將返回到連接池中,等待其它線程的調度。

  這里用到了池化技術,如大家屢見不鮮的線程池、整數池、字符串池、對象池和Http 連接池等等,都是對這個思想的應用。池化技術的思想主要是通過復用對象,以減少每次獲取資源時創建和釋放所帶來的資源消耗,提高資源利用率,這是典型的以空間換取時間的策略。

  數據庫連接池負責分配、管理、釋放數據庫連接,它允許應用服務重復使用數據庫連接,而非重新建立。使用連接池之后,流程是這樣的:

  由此可見,數據庫連接的創建和關閉連接均由連接池來實現。這樣的機制有如下兩個優點:

  1. 封裝關於數據庫訪問的各種參數,實現統一管理
  2. 通過對數據庫的連接池管理,減少網絡開銷並提升數據庫性能

數據庫連接池工作原理剖析

  數據庫連接池的工作原理主要由三部分組成,分別為

  • 連接池的建立
  • 連接池的管理
  • 連接池的關閉
  1. 連接池的建立。應用初始化時,根據配置的最小連接數,在連接池將創建此數目的數據庫連接放到池中,以便使用時能從連接池中獲取。連接池中的連接不能隨意創建和關閉,這樣避免了連接隨意建立和關閉造成的系統開銷。

    Java中提供了很多容器類可以方便的構建連接池,例如Vector、Stack等。

  2. 連接池的管理。連接池管理策略是連接池機制的核心,連接池內連接的分配和釋放對系統的性能有很大的影響。其管理策略是:當客戶請求數據庫連接時,首先查看連接池中是否有空閑連接,如果存在空閑連接,則將連接分配給客戶使用;如果沒有空閑連接,則查看當前所開的連接數是否已經達到最大連接數,如果沒達到就重新創建一個連接給請求的客戶;如果達到就按設定的最大等待時間進行等待,如果超出最大等待時間,則拋出異常給客戶。 當客戶釋放數據庫連接時,先判斷池中的連接數是否超過了設置的最大連接數,如果超過就從連接池中刪除該連接;否則,保留連接,等待再次使用。

  3. 連接池的關閉。應用程序關閉時,關閉連接池中所有連接,釋放所有相關資源。

  在Java這個自由開放的生態中,已經有非常多優秀的開源數據庫連接池可以供大家選擇,比如:DBCP、C3P0、Druid、HikariCP、tomcat-jdbc等。而在Spring Boot 2.x中,對數據源的選擇也緊跟潮流,采用了目前性能最佳的HikariCP。接下來,我們就來具體聊聊HikariCP。

§Java常見數據庫連接池性能比較

  單從性能角度分析,性能從高到低依次是:HikariCP、druid、tomcat-jdbc、dbcp、c3p0。下圖是HikariCP官網給出的性能對比:

img

  從上圖中可以直觀的看出,Hikari 在 獲取和釋放 Connection 和 Statement 方法的 OPS 不是一般的高,那是相當的高,基本上是碾壓其他連接池,這里就不一一點名了。除了 OPS 外,HikariCP 的穩定性也更好,性能毛刺更少。

§數據庫連接池選型 Druid vs HikariCP性能對比

  • 從功能角度考慮,Druid 功能更豐富,除具備連接池基本功能外,還支持sql級監控、擴展、SQL防注入等。最新版甚至有集群監控。兩者的側重點不一樣。
  • 從性能角度考慮,從數據處理速度角度來看,HikariCP確實更強,但Druid由阿里巴巴背書,可支持”雙十一”等最嚴苛的使用場景,並且提供了強大的監控功能,在國內有不少用戶。不過,Spring Boot 2.x已經使用HikariCP作為默認的數據庫連接池,其優秀程度可見一斑。
  • 從監控角度考慮,如果我們有像skywalking、prometheus等組件是可以將監控能力交給這些的,HikariCP也可以將metrics暴露出去。

  HikariCP作為后起之秀,是目前最快的Java數據庫連接池。

§HikariCP為什么這么快

  HikariCP為什么這么快呢?是因為它在如下四個方面做了優化,以提升性能:

  1. 優化並精簡字節碼。使用Java字節碼修改類庫Javassist來生成委托實現動態代理,比JDK Proxy生成的字節碼更少,精簡了很多不必要的字節碼。
  2. 使用自定義的無鎖的、性能更好的並發集合類ConcurrentBag。
  3. 使用自定義的數組類型FastList替代ArrayList。FastList是List接口的精簡實現。
  4. 優化代理和攔截器:減少代碼,例如 HikariCP 的 Statement proxy 只有100行代碼,只有 BoneCP 的十分之一;

  下面是FastList源碼:

/**
 * ArrayList精簡版的、沒有列表檢查的 FastList
 *
 * @author Brett Wooldridge
 */
public final class FastList<T> implements List<T>, RandomAccess, Serializable{
  private static final long serialVersionUID = -4598088075242913858L;

  private final Class<?> clazz;
  private T[] elementData;
  private int size;

  /**
   * 構建一個默認大小為32的列表。
   * @param clazz the Class stored in the collection
   */
  @SuppressWarnings("unchecked")
  public FastList(Class<?> clazz) {
     this.elementData = (T[]) Array.newInstance(clazz, 32);
     this.clazz = clazz;
  }

  /**
   * 構造具有指定大小的列表。
   * @param clazz the Class stored in the collection
   * @param capacity the initial size of the FastList
   */
  @SuppressWarnings("unchecked")
  public FastList(Class<?> clazz, int capacity) {
     this.elementData = (T[]) Array.newInstance(clazz, capacity);
     this.clazz = clazz;
  }

@Override
   public boolean add(T element) {
      //給 list添加屬性
      //如果 size值小於 初始化的值
      if (size < elementData.length) {
         elementData[size++] = element;
      } else {
         // 溢出的代碼
         //elementData 原始32不夠用 需要擴容
         final int oldCapacity = elementData.length;
         final int newCapacity = oldCapacity << 1;
         @SuppressWarnings("unchecked")
         //擴容集合
         final T[] newElementData = (T[]) Array.newInstance(clazz, newCapacity);
         //數組復制
         System.arraycopy(elementData, 0, newElementData, 0, oldCapacity);
         //屬性賦值
         newElementData[size++] = element;
         elementData = newElementData;
      }

      return true;
   }
   /**
    * 貼出ArrayList的get代碼,來看看為什么 FastList 更快
    *  public E get(int index) {
    *   rangeCheck(index);
    *    return elementData(index);
    *   }
    *   ArrayList調用rangeCheck以檢查角標范圍,而FastList直接讀取元素,節約時間
    */
   @Override
   public T get(int index) {
      return elementData[index];
   }
   /**
    * 這個是ArrayList的 remove()代碼, FastList 少了檢查范圍和從頭到尾的 檢查元素動作,速度更快
    *   rangeCheck(index);
    *   modCount++;
    *    E oldValue = elementData(index);
    */
   @Override
   public boolean remove(Object element) {
      for (int index = size - 1; index >= 0; index--) {
         if (element == elementData[index]) {
            final int numMoved = size - index - 1;
            //如果角標不是最后一個 copy一個新的數組結構
            if (numMoved > 0) {
               System.arraycopy(elementData, index + 1, elementData, index, numMoved);
            }
            //如果角標是最后面的 直接初始化為null
            elementData[--size] = null;
            return true;
         }
      }
      return false;
   }

§數據源配置詳解

  由於Spring Boot的自動化配置機制,大部分對於數據源的配置都可以通過配置參數的方式去改變。只有一些特殊情況,比如:更換默認數據源,多數據源共存等情況才需要去修改覆蓋初始化的Bean內容。本節我們主要講Hikari的配置,所以對於使用其他數據源或者多數據源的情況,在之后的教程中學習。

  在Spring Boot自動化配置中,對於數據源的配置可以分為兩類:

  • 通用配置:以spring.datasource.*的形式存在,主要是對一些即使使用不同數據源也都需要配置的一些常規內容。比如:數據庫鏈接地址、用戶名、密碼等。這里就不做過多說明了,通常就這些配置:
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

溫馨提示:driver-class-name用於指定JDBC驅動程序的類名,默認從jdbc url中自動探測。

com.mysql.jdbc.Driver 是 mysql-connector-java 5中的,com.mysql.cj.jdbc.Driver 是 mysql-connector-java 版本6以后的。

  • 數據源連接池配置:以spring.datasource.<數據源名稱>.*的形式存在,比如:Hikari的配置參數就是spring.datasource.hikari.*形式。下面這個是我們最常用的幾個配置項及對應說明:
spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.idle-timeout=500000
spring.datasource.hikari.max-lifetime=540000
spring.datasource.hikari.connection-timeout=60000
spring.datasource.hikari.connection-test-query=SELECT 1

  這些配置的含義:

  • spring.datasource.hikari.minimum-idle: 最小空閑連接,默認值10,小於0或大於maximum-pool-size,都會重置為maximum-pool-size。

  • spring.datasource.hikari.maximum-pool-size: 最大連接數,小於等於0會被重置為默認值10;大於零小於1會被重置為minimum-idle的值

  • spring.datasource.hikari.idle-timeout: 空閑連接超時時間,此屬性控制允許連接在連接池中閑置的最長時間。默認值600000毫秒(10分鍾),大於等於max-lifetime且max-lifetime>0,會被重置為0;不等於0且小於10秒,會被重置為10秒。

    此設置僅適用於maximumPoolSize-minimumIdle的連接。一旦連接池達到最小連接數,空閑連接將不會退出。在超時之前,連接永遠不會退出。值為0意味着空閑連接永遠不會從池中刪除。允許的最小值是10000ms(10秒),默認值值是600000(10分鍾)。

  • spring.datasource.hikari.max-lifetime: 連接最大存活時間,不等於0且小於30秒,會被重置為默認值30分鍾.設置應該比mysql設置的超時時間短

  • spring.datasource.hikari.connection-timeout: 連接超時時間:毫秒,小於250ms,否則被重置為默認值30秒

  • spring.datasource.hikari.connection-test-query: 用於測試連接是否可用的查詢語句

更多完整配置項可查看下表:

name 默認配置validate之后的值 構造器默認值 validate重置 描述
autoCommit TRUE TRUE 自動提交從池中返回的連接
connectionTimeout 30000 SECONDS.toMillis(30) = 30000 如果小於250毫秒,則被重置回30秒 等待來自池的連接的最大毫秒數
idleTimeout 600000 MINUTES.toMillis(10) = 600000 如果idleTimeout+1秒>maxLifetime 且 maxLifetime>0,則會被重置為0(代表永遠不會退出);如果idleTimeout!=0且小於10秒,則會被重置為10秒 連接允許在池中閑置的最長時間
maxLifetime 1800000 MINUTES.toMillis(30) = 1800000 如果不等於0且小於30秒則會被重置回30分鍾 池中連接最長生命周期
connectionTestQuery null null 如果您的驅動程序支持JDBC4,我們強烈建議您不要設置此屬性
minimumIdle 10 -1 minIdle<0或者minIdle>maxPoolSize,則被重置為maxPoolSize 池中維護的最小空閑連接數
maximumPoolSize 10 -1 如果maxPoolSize小於1,則會被重置。當minIdle<=0被重置為DEFAULT_POOL_SIZE則為10;如果minIdle>0則重置為minIdle的值 池中最大連接數,包括閑置和使用中的連接
metricRegistry null null 該屬性允許您指定一個 Codahale / Dropwizard MetricRegistry 的實例,供池使用以記錄各種指標
healthCheckRegistry null null 該屬性允許您指定池使用的Codahale / Dropwizard HealthCheckRegistry的實例來報告當前健康信息
poolName HikariPool-1 null 連接池的用戶定義名稱,主要出現在日志記錄和JMX管理控制台中以識別池和池配置
initializationFailTimeout 1 1 如果池無法成功初始化連接,則此屬性控制池是否將 fail fast
isolateInternalQueries FALSE FALSE 是否在其自己的事務中隔離內部池查詢,例如連接活動測試
allowPoolSuspension FALSE FALSE 控制池是否可以通過JMX暫停和恢復
readOnly FALSE FALSE 從池中獲取的連接是否默認處於只讀模式
registerMbeans FALSE FALSE 是否注冊JMX管理Bean(MBeans)
catalog null driver default 為支持 catalog 概念的數據庫設置默認 catalog
connectionInitSql null null 該屬性設置一個SQL語句,在將每個新連接創建后,將其添加到池中之前執行該語句。
driverClassName null null HikariCP將嘗試通過僅基於jdbcUrl的DriverManager解析驅動程序,但對於一些較舊的驅動程序,還必須指定driverClassName
transactionIsolation null null 控制從池返回的連接的默認事務隔離級別
validationTimeout 5000 SECONDS.toMillis(5) = 5000 如果小於250毫秒,則會被重置回5秒 連接將被測試活動的最大時間量
leakDetectionThreshold 0 0 如果大於0且不是單元測試,則進一步判斷:(leakDetectionThreshold < SECONDS.toMillis(2) or (leakDetectionThreshold > maxLifetime && maxLifetime > 0),會被重置為0 . 即如果要生效則必須>0,而且不能小於2秒,而且當maxLifetime > 0時不能大於maxLifetime 記錄消息之前連接可能離開池的時間量,表示可能的連接泄漏
dataSource null null 這個屬性允許你直接設置數據源的實例被池包裝,而不是讓HikariCP通過反射來構造它
schema null driver default 該屬性為支持模式概念的數據庫設置默認模式
threadFactory null null 此屬性允許您設置將用於創建池使用的所有線程的java.util.concurrent.ThreadFactory的實例。
scheduledExecutor null null 此屬性允許您設置將用於各種內部計划任務的java.util.concurrent.ScheduledExecutorService實例

§數據源配置案例

  數據庫連接池properties文件配置信息:

###數據源配置###

#默認就是hikari,可缺省
#spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mysql?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true
spring.datasource.username=root
spring.datasource.password=root

#默認30000ms,即30s
#spring.datasource.hikari.connection-timeout=30000
#存活時間,默認600000ms,即10min
spring.datasource.hikari.idle-timeout=600000
#連接池的最大尺寸(閑置連接+正在使用的連接),默認10
spring.datasource.hikari.maximum-pool-size=200
#最小空閑連接數,默認10
spring.datasource.hikari.minimum-idle=50
spring.datasource.hikari.pool-name=私有連接池

  HikariDataSource在應用啟動后,第一次數據庫交互的時候加載連接池信息,這就是因為Spring Boot 2.x連接數據庫用到了懶加載。

§Reference


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM