你好,我是 yes歉眷。
今天給大家剖析下一個叫 ConcurrentBag 的并發(fā)集合類牺六,對 C# 熟悉的同學(xué)應(yīng)該聽過這個名字,不過我今天介紹的是 HikariCP 中的 ConcurrentBag汗捡。
我們知道 SpringBoot 默認(rèn)連接池就是 HikariCP淑际,而 HikariCP 就是以快著稱的,而這個快離不開 ConcurrentBag。
如果你看過很多源碼你就會發(fā)現(xiàn)好多框架都會自定義集合類庸追,因為 JDK 通用的集合需要照顧到很多場景霍骄,而定制化肯定優(yōu)于普適化。
像 HikariCP 就沒有用 ArrayList 而是定義了一個 FastList淡溯,因為 ArrayList 每次 get 都會有范圍檢查读整,并且 remove 是從前往后遍歷的。
而在 HikariCP 這個場景每次 get 范圍檢查沒有必要咱娶,并且 remove 的時候從后往前遍歷更好米间,所以就定制化了。
HikariCP 還有很多優(yōu)化膘侮,這篇文章我們就談?wù)勂渲兄磺簿褪墙裉斓闹鹘蔷褪?ConcurrentBag 。
不過今天的目的不是為了分析 HikariCP 琼了,而只是介紹這個集合類逻锐。
從它身上找點優(yōu)化的思路,到時候像面試官問你如何設(shè)計一個連接池的時候就可以搬出來:“哎呀雕薪,我有個優(yōu)化思路昧诱。”
ConcurrentBag
一般而言我們設(shè)計一個連接池的初始想法是用鎖來保證線程安全所袁,或者用一些線程安全的并發(fā)容器來存儲連接盏档。
而 HikariCP 不滿足于此,它專門設(shè)計了 ConcurrentBag 用來存數(shù)據(jù)庫連接燥爷,當(dāng) HikariPool#getConnection 的時候就是去 ConcurrentBag 拿連接蜈亩。
ConcurrentBag 整體就是無鎖設(shè)計,有三個重要的成員變量:
- ThreadLocal 緩存前翎,加快本地連接獲取速度
- CopyOnWriteArrayList稚配,寫時拷貝List
- SynchronousQueue,無存儲的等待隊列
獲取數(shù)據(jù)庫連接基本流程如下:
- 當(dāng)取連接的時候會先去 ThreadLocal 去找以前用過的連接鱼填,如果找到連接狀態(tài)是可以使用的話拿直接返回药有。
(ThreadLocal 是本地資源,每個線程都優(yōu)先去自己本地去找苹丸,所以競爭也更少愤惰,需要遍歷的連接也更少,所以速度就更快) - 找不到再去 sharedList 這個共享的寫時復(fù)制列表中查找可用連接赘理。
- 如果再找不到宦言,則通過 handoffQueue 等待可用的連接,如果超過一定時間則返回 null商模。
其實這種思想很簡單奠旺。
每個線程一開始本地資源肯定是空的蜘澜,然后每個線程把自己用過的連接存起來,之后優(yōu)先用存著的鏈接响疚。
久而久之每個線程都會有自己的本地存儲的連接鄙信,這樣大家都用自己的就少了競爭,那速度不就快了忿晕?
我們再來看下取連接的源碼装诡,里面還是有一些細(xì)節(jié)的。
其實應(yīng)該叫借連接践盼,因為要還的鸦采,而且也不是把連接從 ConcurrentBag 移除,只是返回一個引用罷了咕幻。
細(xì)節(jié)已經(jīng)在代碼上標(biāo)注了渔伯,這里強調(diào)一下借連接不是移除連接,別的線程還是能通過 sharedList 找到這個連接的肄程,無非這個連接如果被占用則狀態(tài)是 STATE_IN_USE
锣吼,這樣別的線程就不會用這個連接了。
總體思路就是從本地找蓝厌,沒有的話再去每個線程都能訪問的 sharedList 找吐限,再沒有就等著。
這里還有個竊取的概念褂始,其實沒什么花頭,就是充分利用連接描函。
無非就是本來屬于某個線程的本地連接崎苗,當(dāng)它歸還連接的時,恰巧有另一個線程從 sharedList 遍歷找到這個連接舀寓,這時候連接的狀態(tài)是 STATE_NOT_IN_USE
胆数,那么這個連接就會被另一個線程也保存到 ThreadLocal 中了。
這就是竊取互墓,我們再來看下歸還連接的代碼必尼,連接就是在這里保存到 ThreadLocal 中的。
我在《HikariCP數(shù)據(jù)庫連接池實戰(zhàn)》這本書中看到篡撵,歸還連接的代碼在 HikariCP 2.6.0 是長下面這個樣子的
先停下來想想看有沒有啥問題判莉?
當(dāng)前歸還連接的線程需要等這個連接被其他線程取走時或者沒有等待線程時才能擺脫這個循環(huán)。
但是會出現(xiàn)一種情況:在設(shè)置連接為可用時育谬,這個連接已經(jīng)被其他線程借走了券盅,然后當(dāng)前線程還傻傻的執(zhí)行循環(huán),而恰巧等待線程一直有膛檀,但是每次 handoffQueue.offer 就是沒線程取锰镀,然后 yield 娘侍,如此往復(fù)。
這就造成明明連接已經(jīng)歸還了泳炉,而歸還的線程還做無用功的自旋操作憾筏,所以就做優(yōu)化成上面的代碼,如果bagEntry.getState() != STATE_NOT_IN_USE
說明已經(jīng)被別的線程借去用了花鹅,所以直接 return氧腰。
再提一提 CopyOnWriteArrayList 吧。
連接池是一個典型的讀多寫少的場景翠胰,所以寫時復(fù)制用在此處再合適不過了容贝。
簡單的說:寫操作的時候會復(fù)制當(dāng)前的 list 來做修改,等修改完了再替換老的 list之景。
在替換之前讀的線程讀取的是老的 list 的數(shù)據(jù)斤富,這樣就能做到讀的時候是無鎖的。
寫時復(fù)制的缺點就是內(nèi)存的占用锻狗,因為需要拷貝一份數(shù)據(jù)满力,如果數(shù)據(jù)很大的話那就需要考慮內(nèi)容的占用量了。
比如操作系統(tǒng)進程的 fork 操作也會用到寫時復(fù)制轻纪,子進程和父進程一開始共享數(shù)據(jù)油额,當(dāng)有修改的時候就會拷貝一份。
在 Redis 的 BGSAVE 命令或者 BGREWRITEAOF 命令的過程中就會 fork 子進程來進行后臺操作刻帚,而此時 Redis 的哈希表擴容的負(fù)載因子就會變大潦嘶,來避免 fork 期間不必要的內(nèi)存寫入操作 (擴容)。
最后
所以 ConcurrentBag 的優(yōu)化思路就是本地緩存有的去本地緩存找連接崇众,找不到就去公共的 sharedList 去找掂僵,還找不到就等著。
通過將連接本地存儲化來減少競爭顷歌,又根據(jù)連接池讀多寫少的特性用 CopyOnWriteArrayList 來實現(xiàn) sharedList 锰蓬。
當(dāng)然還有像上面 borrow 和 requite 的一些細(xì)節(jié)也值得品味,追求極致速度就需要扣細(xì)節(jié)眯漩。
更多文章可看我的文章匯總:https://github.com/yessimida/yes 歡迎 star !
我是 yes芹扭,從一點點到億點點,歡迎在看赦抖、轉(zhuǎn)發(fā)舱卡、留言,我們下篇見队萤。
巨人的肩膀
《HikariCP數(shù)據(jù)庫連接池實戰(zhàn)》