hikari簡介以及相關的概念
"Simplicity is prerequisite for reliability."
- Edsger Dijkstra
hikari,日語中“光”的意思,作者為這個數據庫連接池命名為光,寓意是像光一樣快。在分析hikariCP之前簡單介紹下JDBC和數據庫連接池。
JDBC
全稱Java Database Connectivity,java入門課本中基本都會介紹到的部分。
以常見的MySQL數據庫為例,JDBC可以簡單概括為就是一個jar包,這個jar包中提供了相應的interface與class,封裝好了與MySQL服務端連接的協議,通過Java代碼就可以實現mysql client上的select、update、delete等功能。
原始的JDBC使用姿勢是這樣的(爺青回):
try {
// 初始化驅動類com.mysql.jdbc.Driver
Class.forName("com.mysql.jdbc.Driver");
// 獲取connection連接
conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test?characterEncoding=UTF-8","root", "admin");
// 查詢
sql = "select * from test"
pstmt = (PreparedStatement) conn.prepareStatement(sql);
rs = (ResultSet) pstmt.executeQuery();
// 后續將ResultSet轉化為對象返回...
} catch (ClassNotFoundException e) {
e.printStackTrace();
}catch (SQLException e) {
e.printStackTrace();
}
數據庫連接池(Connection Pool)
以上DriverManager.getConnection
建立jdbc連接的過程是很昂貴的,需要經歷 TCP握手+MySQL認證,如果服務在不斷并發處理多個請求的時候每次都重新建立JDBC連接并且在請求結束后關閉JDBC連接,那么將會導致:
- 網絡IO多,因為不斷的TCP握手和TCP關閉,大量新建后立刻關閉的TIME_WAIT狀態的TCP連接占用系統資源
- 數據庫負載高,數據庫也要重新為連接建立各種數據結構,并且在短暫的查詢結束后又要銷毀
- 查詢耗時長,多了TCP握手和MySQL認證的耗時。同時,不斷產生又回收掉JDBC連接資源會頻繁觸發GC
為了能夠 復用已經辛辛苦苦建立好的JDBC連接,就有了數據庫連接池的概念,同理線程池、redis連接池、HBase連接池等等都同理。
數據庫連接池應該具有的功能
- 在連接應用啟動的時候建立連接,并且保存到Java的容器或自定義的容器中維護
- 在應用運行的過程中,間歇性測試連接的可用性(
select 1
),踢出不可用的連接,加入新的連接 - 在應用結束后關閉JDBC連接
Spring-JDBC
基于以上原JDBC的使用方式可以看出,JDBC在使用上存在大量可以封裝起來的功能(connection的獲取、構造PreparedStatement、connection的回收等)。總之,Spring-JDBC通過對JDBC的封裝,簡化了SQL的執行步驟,我們只需要輸入sql語句,框架會輸出sql的執行結果(類似于mysql-client控制臺的效果)。
如上圖,有了Spring-JDBC等框架封裝JDBC連接獲取,prepareStatement賦值,JDBC連接的釋放,使用者就只需要寫好sql,調用Spring-JDBC提供的JdbcTemplate
類封裝的方法,等待獲取結果即可。
JdbcTemplate類會通過Spring依賴注入等方式設置好它的dataSource
屬性,而這個dataSource就是一個數據連接池(Connection Pool,CP),CP通過選擇合適的數據結構來動態維護著與MySQL的Jdbc連接。整個封裝起來對使用者來說無需感知,只需要知道輸入SQL語句和輸出結果就好。
hikari核心功能
HikariDataSource
HikariDataSource的繼承結構
正如上圖所示,dataSource代表著一個數據庫連接池,而hikari的數據庫連接池就是HikariDataSource
,這個類是hikari的核心類。
HikariDataSource的繼承關系如上圖,重點關注HikariConfig
和DataSource
。
-
DataSource,是一個接口,就是數據庫連接池在Java類中的體現,對JDBC框架來說不需要關注Connection怎么來的,只需要調這個接口的方法獲取即可
DataSource - HikariConfig,hikari配置類,有著jdbcUrl、用戶名密碼、超時時間、數據庫連接池連接數等成員變量屬性
HikariDataSource的成員結構
HikariDataSource的主要成員結構如下圖:
- isShutdown,一個AtomicBoolean(cas樂觀鎖)用來標識該dataSource是否已經關閉
- fastPathPool 和 pool,都是指向HikariPool結構的引用(Hikari優化用來提升性能的手段)
- 若干繼承自HikariConfig的數據庫連接池配置屬性
HikariPool
HouseKeeper管家
HikariPool的主要屬性如上圖所示,主要通過30s執行一次的定時任務HouseKeeper
來維護管理連接池中的連接。
HouseKeeper任務執行流程的偽代碼如下:
def run():
# hikari的連接池維護機制大量依賴時間戳,如果發現系統時間倒退了,則會將連接池清空重來
if detect_retrograde_time():
softEvictConnections()
return
if detect_forward_time(): # 如果發現時間推前了,打日志,警告維持的連接會提前退休
log()
if idleTimeout > 0L && config.getMinimumIdle() < config.getMaximumPoolSize():
# 清理過期連接
for connection in connectionBag_NotInUse:
if is_timeout(connection):
closeConnection(connection)
# 初始化或清理過后
fillPool() # 填充連接池,保證最小的連接數量(idleConnections),同時保證不超過最大連接數量(maximumPoolSize)
從偽代碼中可以看出,HouseKeeper主要的工作分為:
- 檢測系統時間的準確性,系統時間被用戶篡改過,則處理清空連接池
- 對空閑的連接,根據idleTimeout屬性清理過期連接
- 在初始化或清理過后,填充新的連接到連接池中
關閉連接的過程(closeConnection)和填充的過程(fillPool)則分別是通過向上圖中的closeConnectionExecutor
和addConnectionExecutor
連接池提交異步任務的方式,分別執行JDBC連接關閉和異步的建立JDBC連接的。
hikari優化的連接池存儲容器 - ConcurrentBag
通過上面的分析,我們知道了hikari是通過在HikariPool這樣的數據結構中執行30s一次的定時任務動態關閉/新增連接到連接池的。維護連接池中的連接就需要用到一種 容器,由于新增和關閉連接都是通過線程池異步執行的,而且getConnection()
的操作大多數情況下是并發的,必然涉及到支持 并發。在Hikari中是通過自定義的容器類ConcurrentBag
來維護JDBC連接的。
ConcurrentBag的結構
ConcurrentBag的結構如下圖,重點關注
- sharedList和threadList,是兩個關于PoolEntry(JdbcConnection的封裝)的列表,用來實際存儲連接池中維護的Jdbc連接
- listener,是指向HikariPool的引用,當ConcurrentBag獲取連接時發現用完了,則通過listener的回調接口請求HikariPool多擴充點Jdbc連接入池
- handoffQueue,和listener相關,HikariPool擴充連接池后將新的連接通過該隊列提供給正在等待的線程,正在
getConnection
的線程會在queue的另一端阻塞住,等待擴充連接后喂給它們
PoolEntry,可以簡單理解為Hikari對JdbcConnection的一層封裝,有四種狀態
- int STATE_NOT_IN_USE = 0; 沒在用
- int STATE_IN_USE = 1; 使用中
- int STATE_REMOVED = -1; 已移除
- int STATE_RESERVED = -2; 預定
ConcurrentBag#add
新增的接口很簡單,這里直接貼出源碼。
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
while (waiters.get() > 0 && !handoffQueue.offer(bagEntry)) {
yield();
}
}
從代碼中可以看出主要的工作分為兩步:
- sharedList.add()
- 如果有等待中的線程(正在getConnection),則嘗試將新加入的JDBC連接通過隊列handoffQueue喂給它
ConcurrentBag#remove
- 通過cas確保當前這個connection在使用中或被預定,使用中是在getConnection的時候通過探活(select 1)發現連接已經已經失效時,將其關閉;被預定是在HouseKeeper管家第2步對空閑的連接清理的時候,提前將要清理的連接鎖住,并且執行close操作時。
- sharedList.remove(bagEntry); -- 從list中移除
ConcurrentBag#borrow
borrow方法的偽代碼如下。
def borrow():
if (bagEntry = find_from_threadList()) != null: # 1.優先從threadList中找
return bagEntry
waiters++
try:
for bagEntry in sharedList: # 2. 從sharedList中找
if not_in_use(bagEntry):
if waiters > 1:
# 2.1 如果從sharedList中找到了,但是有waiting的,大概率是在并發情況下搶到了別人的,那就幫他再申請一個JDBC連接
listener.addBagItem(waiters - 1);
return bagEntry
# 3. sharedList中沒找到,則申請新的JDBC連接
listener.addBagItem(waiting);
# 4. 申請了新的JDBC連接后,站在handoffQueue的一邊,等待JDBC連接創建好的結果
while(not_timeout()):
bagEntry = handoffQueue.poll()
if not_in_use(bagEntry):
return bagEntry
finally:
waiters--
ConcurrentBag、hikari與Spring-JDBC
上面的解釋可能太過底層和抽象,基于上面對ConcurrentBag主要方法的源碼分析,我們這里可以將hikari的ConcurrentBag與Spring-JDBC整個串起來,分析一條sql語句執行的整個過程。
以語句select * from xxx
為例,
- 在業務代碼層面通過調用Spring-JDBC提供的
JdbcTemplate
的query方法,獲取查詢結果 - JdbcTemplate調用內部的execute方法時,調用了
HikariDataSource#getConnection
方法(dataSource可以通過手動注入,或SpringIOC注入) - HikariDataSource通過向其內部維護的連接池對象
HikariPool
請求getConnection獲取連接(注意,這里有兩個HikariPool,是hikari的一種優化手段,下文中會詳細分析) - HikariPool通過向其內部維護的
connectionBag
(即ConcurrentBag對象)借用(borrow
)來獲取連接,借用的過程就是從寫時拷貝list sharedList或threadLocal threadList獲取連接,最終再一層層返回給JdbcTemplate,由JdbcTemplate執行sql語句,并將最終結果返回給業務代碼 - 在第4步中,可能存在ConcurrentBag中維護的連接不夠用的情況,這時候會通過listener指向的HikariPool請求擴充連接池中的連接
綜上,我們可以看出,最終一條sql是通過這樣層層抽象封裝來實現的,
- HikariDataSource只是為了對Spring-JDBC的
DataSource
接口做適配而產生的 - HikariPool才是Hikari的核心功能,它像是一個專門負責管理DB連接的管家,背著一個名叫ConcurrentBag的重重的背包,在你(業務側)需要JDBC連接的時候提供給你,同時管家自己會是不是檢查和擴充背包里的連接(就像跟女神和高富帥一起爬山時跟在最后背包的工具人)淚目( ? ^ ? )
Hikari的優化點
通過上面的分析,讀者應該已經清楚了Hikari的大致原理,有了大致的了解之后,Hikari的代碼實現的很多細節通過閱讀源碼也可以細化了解。我們這里回到最關鍵的問題,Hikari為什么這么快,為什么敢用 光 來命名自己。
字節碼精簡
字節碼精簡:優化代碼,直到編譯后的字節碼最少,這樣,CPU緩存可以加載更多的程序代碼;
優化代理和攔截器:減少代碼,例如HikariCP的Statement proxy只有100行代碼,只有BoneCP的十分之一
以上是從網上搜到的,確實可以體現出Hikari極致優化的特性,但是對性能影響可能微乎其微。
fastPathPool 和 pool
如上面關于HikariDataSource與HikariPool的關系時,我們可以看到HikariDataSource內部有兩個指向HikariPool的引用。這里也是Hikari優化性能的一種手段。
HikariDataSource有兩種初始化的方式:
- 一種是無參數的初始化方式,這種時候當執行到getConnection的時候,會通過 懶漢單例模式 懶加載初始化HikariPool,賦值給pool引用,懶漢模式涉及到并發情況,所以一般會考慮采用 Double-Check-Lock(DCL) 方式加鎖,DCL方式中涉及到JVM的字節碼指令重排優化的問題,所以需要將pool引用設置為volatile,廢棄掉JVM的字節碼指令優化。(【5】Java中DCL(Double-Check-Lock)對volatile必要性的疑惑)但是同時因為volatile關鍵字,導致pool相關的加載都會有性能問題。所以獲取HikariPool時優先通過fastPathPool引用。
-
另一種是帶參數的初始化,在構造函數中同時將初始化完的HikariPool賦值給pool和fastPathPool引用,在獲取HikariPool引用的時候總是優先獲取fastPathPool,以防止volatile關鍵字對性能的影響
構造函數
自定義的容器類型
1、 定義數組類型(FastStatementList)代替ArrayList:避免每次get()調用都要進行range check,避免調用remove()時的從頭到尾的掃描;【6】【追光者系列】HikariCP源碼分析之FastList
2、 自定義集合類型(ConcurrentBag):提高并發讀寫的效率;
通過之前對ConcurrentBag的borrow方法的了解,我們知道,hikariPool是通過ConcurrentBag#borrow方法來獲取連接的,而ConcurrentBag中的邏輯則是:優先從threadList(ThreadLocal)獲取;獲取不到時再從sharedList(CopyOnWriteArrayList)獲取
ThreadLocal:例如業務代碼執行了兩次SQL,獲取了兩次JDBC連接,在第一次執行完畢之后ConcurrentBag會回收該連接,但是會回收到ThreadLocal中。當業務代碼第二次執行SQl需要獲取JDBC連接時,只要是同一個線程,則會從ThreadLocal中獲取到連接。(關于ThreadLocal的原理,可以參考筆者的往期文章 【7】圖解分析ThreadLocal的原理與應用場景)
CopyOnWriteArrayList:寫時拷貝技術,并發情況下線程安全版本的ArrayList,寫時拷貝技術源自于unix系統的fork系統調用,指的是讀操作時不加鎖,只有當寫操作執行的時候鎖住整個list,然后執行替換(貍貓換太子)
擴展知識
事務管理器transactionManager
在需要支持事務操作的方法上,我們通常會加上這樣的注解, @Transactional(rollbackFor = Exception.class, value = "txManagerXXX")
, 用來表示注解包裹住的方法內部是一個事務,需要符合事務的特性(如原子性)
在Spring中的實現是通過AOP切面技術生成了動態代理,封裝了@Transactional注解的方法
JNDI
JNDI,全稱是 Java Naming and Directory Interface,Java 命名與目錄接口,也是跟原始的JDBC用法息息相關的概念。數據庫的實現有很多種,Java服務是通過JDBC驅動類來獲取JDBC連接的,那么原始的JDBC用法就會有幾種問題:
- 數據庫的地址、賬戶名、密碼、連接池的參數發生改變,要修改代碼
- 數據庫可能從MySQL改為Oracle或者其它DB,需要修改底層代碼,換JDBC驅動jar包,由此會引發一系列的相關功能的回歸
基于以上問題,J2EE規范提出了JNDI的規范,對于業務代碼來說只需要關心獲取的DataSource是哪個(通過naming-名字),不需要知道這個DataSource背后的細節(url地址、賬戶名密碼等),即findResouceByName
,而resources的相關參數是配置在xml文件里的。
經常用Spring的讀者可以看出,這就是SpringIOC容器提供的getBean(String beanName)
方法,即Spring的xml文件,所以SpringIOC就是在J2EE的規范上實現的。(【8】Spring IOC 前世今生之 JDNI - binarylei - 博客園)
references
- ^ Introduction to HikariCP | Baeldung
- ^ GitHub - brettwooldridge/HikariCP: 光 HikariCP?A solid, high-performance, JDBC connection pool at last.
- ^ HikariCP連接池 - 簡書
- ^ 源碼詳解系列(八) ------ 全面講解HikariCP的使用和源碼 - 子月生 - 博客園
- ^ Java中DCL(Double-Check-Lock)對volatile必要性的疑惑_u010131029的博客-CSDN博客
- ^ 【追光者系列】HikariCP源碼分析之FastList_weixin_34304013的博客-CSDN博客
- ^ 圖解分析ThreadLocal的原理與應用場景
- ^ Spring IOC 前世今生之 JDNI - binarylei - 博客園