在項目開發中,爲提高系統性能,減小 IO 開銷,本地緩存是必不可少的。最多見的本地緩存是 Guava 和 Caffeine,本篇文章將爲你們介紹 Caffeine。html
Caffeine 是基於 Google Guava Cache 設計經驗改進的結果,相較於 Guava 在性能和命中率上更具備效率,你能夠認爲其是 Guava Plus。git
毋庸置疑的,你應該儘快將你的本地緩存從 Guava 遷移至 Caffeine,本文將重點和 Guava 對比兩者性能佔據,給出本地緩存的最佳實踐,以及遷移策略。github
2、PK Guava
2.1 功能
![](http://static.javashuo.com/static/loading.gif)
從功能上看,Guava 已經比較完善了,知足了絕大部分本地緩存的需求。Caffine 除了提供 Guava 已有的功能外,同時還加入了一些擴展功能。web
2.2 性能
Guava 中其讀寫操做夾雜着過時時間的處理,也就是你在一次 put 操做中有可能會作淘汰操做,因此其讀寫性能會受到必定影響。redis
Caffeine 在讀寫操做方面完爆 Guava,主要是由於 Caffeine 對這些事件的操做是異步的,將事件提交至隊列(使用 Disruptor RingBuffer),而後會經過默認的 ForkJoinPool.commonPool(),或本身配置的線程池,進行取隊列操做,而後再進行後續的淘汰、過時操做。算法
如下性能對比來自 Caffeine 官方提供數據:數據庫
(1)在此基準測試中,從配置了最大大小的緩存中,8 個線程併發讀:緩存
(2)在此基準測試中,從配置了最大大小的緩存中,6個線程併發讀、2個線程併發寫:微信
![](http://static.javashuo.com/static/loading.gif)
(3)在此基準測試中,從配置了最大大小的緩存中,8 個線程併發寫:併發
![](http://static.javashuo.com/static/loading.gif)
2.3 命中率
緩存的淘汰策略是爲了預測哪些數據在短時間內最可能被再次用到,從而提高緩存的命中率。Guava 使用 S-LRU 分段的最近最少未使用算法,Caffeine 採用了一種結合 LRU、LFU 優勢的算法:W-TinyLFU,其特色是:高命中率、低內存佔用。
2.3.1 LRU
Least Recently Used:若是數據最近被訪問過,未來被訪問的機率也更高。每次訪問就把這個元素放到隊列的頭部,隊列滿了就淘汰隊列尾部的數據,即淘汰最長時間沒有被訪問的。
須要維護每一個數據項的訪問頻率信息,每次訪問都須要更新,這個開銷是很是大的。
其缺點是,若是某一時刻大量數據到來,很容易將熱點數據擠出緩存,留下來的極可能是隻訪問一次,從此不會再訪問的或頻率極低的數據。好比外賣中午時候訪問量突增、微博爆出某明星糗事就是一個突發性熱點事件。當事件結束後,可能沒有啥訪問量了,可是因爲其極高的訪問頻率,致使其在將來很長一段時間內都不會被淘汰掉。
2.3.2 LFU
Least Frequently Used:若是數據最近被訪問過,那麼未來被訪問的機率也更高。也就是淘汰必定時間內被訪問次數最少的數據(時間局部性原理)。
須要用 Queue 來保存訪問記錄,能夠用 LinkedHashMap 來簡單實現一個基於 LRU 算法的緩存。
其優勢是,避免了 LRU 的缺點,由於根據頻率淘汰,不會出現大量進來的擠壓掉老的,若是在數據的訪問的模式不隨時間變化時候,LFU 可以提供絕佳的命中率。
其缺點是,偶發性的、週期性的批量操做會致使LRU命中率急劇降低,緩存污染狀況比較嚴重。
2.3.3 TinyLFU
TinyLFU 顧名思義,輕量級LFU,相比於 LFU 算法用更小的內存空間來記錄訪問頻率。
TinyLFU 維護了近期訪問記錄的頻率信息,不一樣於傳統的 LFU 維護整個生命週期的訪問記錄,因此他能夠很好地應對突發性的熱點事件(超過必定時間,這些記錄再也不被維護)。這些訪問記錄會做爲一個過濾器,當新加入的記錄(New Item)訪問頻率高於將被淘汰的緩存記錄(Cache Victim)時纔會被替換。流程以下:
![](http://static.javashuo.com/static/loading.gif)
儘管維護的是近期的訪問記錄,但仍然是很是昂貴的,TinyLFU 經過 Count-Min Sketch 算法來記錄頻率信息,它佔用空間小且誤報率低,關於 Count-Min Sketch 算法能夠參考論文:pproximating Data with the Count-Min Data Structure
2.3.4 W-TinyLFU
W-TinyLFU 是 Caffeine 提出的一種全新算法,它能夠解決頻率統計不許確以及訪問頻率衰減的問題。這個方法讓咱們從空間、效率、以及適配舉證的長寬引發的哈希碰撞的錯誤率上作均衡。
下圖是一個運行了 ERP 應用的數據庫服務中各類算法的命中率,實驗數據來源於 ARC 算法做者,更多場景的性能測試參見官網:
![](http://static.javashuo.com/static/loading.gif)
W-TinyLFU 算法是對 TinyLFU算法的優化,可以很好地解決一些稀疏的突發訪問元素。在一些數目不多但突發訪問量很大的場景下,TinyLFU將沒法保存這類元素,由於它們沒法在短期內積累到足夠高的頻率,從而被過濾器過濾掉。W-TinyLFU 將新記錄暫時放入 Window Cache 裏面,只有經過 TinLFU 考察才能進入 Main Cache。大體流程以下圖:
![](http://static.javashuo.com/static/loading.gif)
3、最佳實踐
3.1 實踐1
配置方式:設置 maxSize
、refreshAfterWrite
,不設置 expireAfterWrite
存在問題:get 緩存間隔超過 refreshAfterWrite
後,觸發緩存異步刷新,此時會獲取緩存中的舊值
適用場景:
-
緩存數據量大,限制緩存佔用的內存容量 -
緩存值會變,須要刷新緩存 -
能夠接受任什麼時候間緩存中存在舊數據
![](http://static.javashuo.com/static/loading.gif)
設置 maxSize
、refreshAfterWrite
,不設置 expireAfterWrite
3.2 實踐2
配置方式:設置 maxSize
、expireAfterWrite
,不設置 refreshAfterWrite
存在問題:get 緩存間隔超過 expireAfterWrite
後,針對該 key,獲取到鎖的線程會同步執行 load,其餘未得到鎖的線程會阻塞等待,獲取鎖線程執行延時過長會致使其餘線程阻塞時間過長
適用場景:
-
緩存數據量大,限制緩存佔用的內存容量 -
緩存值會變,須要刷新緩存 -
不能夠接受緩存中存在舊數據 -
同步加載數據延遲小(使用 redis 等)
![](http://static.javashuo.com/static/loading.gif)
設置 maxSize
、expireAfterWrite
,不設置refreshAfterWrite
3.3 實踐3
配置方式:設置 maxSize
,不設置 refreshAfterWrite
、expireAfterWrite
,定時任務異步刷新數據
存在問題:須要手動定時任務異步刷新緩存
適用場景:
-
緩存數據量大,限制緩存佔用的內存容量 -
緩存值會變,須要刷新緩存 -
不能夠接受緩存中存在舊數據 -
同步加載數據延遲可能會很大
![](http://static.javashuo.com/static/loading.gif)
設置 maxSize,不設置 refreshAfterWrite
、expireAfterWrite
,定時任務異步刷新數據
3.4 實踐4
配置方式:設置 maxSize
、refreshAfterWrite
、expireAfterWrite
,refreshAfterWrite
< expireAfterWrite
存在問題:
-
get 緩存間隔在 refreshAfterWrite
和expireAfterWrite
之間,觸發緩存異步刷新,此時會獲取緩存中的舊值 -
get 緩存間隔大於 expireAfterWrite
,針對該 key,獲取到鎖的線程會同步執行 load,其餘未得到鎖的線程會阻塞等待,獲取鎖線程執行延時過長會致使其餘線程阻塞時間過長
適用場景:
-
緩存數據量大,限制緩存佔用的內存容量 -
緩存值會變,須要刷新緩存 -
能夠接受有限時間緩存中存在舊數據 -
同步加載數據延遲小(使用 redis 等)
![](http://static.javashuo.com/static/loading.gif)
設置 maxSize
、refreshAfterWrite
、expireAfterWrite
4、遷移指南
4.1 切換至 Caffeine
在 pom 文件中引入 Caffeine 依賴:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
Caffeine 兼容 Guava API,從 Guava 切換到 Caffeine,僅須要把 CacheBuilder.newBuilder()
改爲 Caffeine.newBuilder()
便可。
4.2 Get Exception
須要注意的是,在使用 Guava 的 get()
方法時,當緩存的 load()
方法返回 null
時,會拋出 ExecutionException
。切換到 Caffeine 後,get()
方法不會拋出異常,但容許返回爲 null
。
Guava 還提供了一個getUnchecked()
方法,它不須要咱們顯示的去捕捉異常,可是一旦 load()
方法返回 null
時,就會拋出 UncheckedExecutionException
。切換到 Caffeine 後,再也不提供 getUnchecked()
方法,所以須要作好判空處理。
本文分享自微信公衆號 - 浪尖聊大數據(bigdatatip)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。