在項目開發中,爲提高系統性能,減小 IO 開銷,本地緩存是必不可少的。最多見的本地緩存是 Guava 和 Caffeine,本篇文章將爲你們介紹 Caffeine。html
Caffeine 是基於 Google Guava Cache 設計經驗改進的結果,相較於 Guava 在性能和命中率上更具備效率,你能夠認爲其是 Guava Plus。git
毋庸置疑的,你應該儘快將你的本地緩存從 Guava 遷移至 Caffeine,本文將重點和 Guava 對比兩者性能佔據,給出本地緩存的最佳實踐,以及遷移策略。github
2、PK Guava
2.1 功能
從功能上看,Guava 已經比較完善了,知足了絕大部分本地緩存的需求。Caffine 除了提供 Guava 已有的功能外,同時還加入了一些擴展功能。web
2.2 性能
Guava 中其讀寫操做夾雜着過時時間的處理,也就是你在一次 put 操做中有可能會作淘汰操做,因此其讀寫性能會受到必定影響。redis
Caffeine 在讀寫操做方面完爆 Guava,主要是由於 Caffeine 對這些事件的操做是異步的,將事件提交至隊列(使用 Disruptor RingBuffer),而後會經過默認的 ForkJoinPool.commonPool(),或本身配置的線程池,進行取隊列操做,而後再進行後續的淘汰、過時操做。算法
如下性能對比來自 Caffeine 官方提供數據:數據庫
(1)在此基準測試中,從配置了最大大小的緩存中,8 個線程併發讀:緩存
(2)在此基準測試中,從配置了最大大小的緩存中,6個線程併發讀、2個線程併發寫:微信
(3)在此基準測試中,從配置了最大大小的緩存中,8 個線程併發寫:併發
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)時纔會被替換。流程以下:
儘管維護的是近期的訪問記錄,但仍然是很是昂貴的,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 算法做者,更多場景的性能測試參見官網:
W-TinyLFU 算法是對 TinyLFU算法的優化,可以很好地解決一些稀疏的突發訪問元素。在一些數目不多但突發訪問量很大的場景下,TinyLFU將沒法保存這類元素,由於它們沒法在短期內積累到足夠高的頻率,從而被過濾器過濾掉。W-TinyLFU 將新記錄暫時放入 Window Cache 裏面,只有經過 TinLFU 考察才能進入 Main Cache。大體流程以下圖:
3、最佳實踐
3.1 實踐1
配置方式:設置 maxSize
、refreshAfterWrite
,不設置 expireAfterWrite
存在問題:get 緩存間隔超過 refreshAfterWrite
後,觸發緩存異步刷新,此時會獲取緩存中的舊值
適用場景:
-
緩存數據量大,限制緩存佔用的內存容量 -
緩存值會變,須要刷新緩存 -
能夠接受任什麼時候間緩存中存在舊數據
設置 maxSize
、refreshAfterWrite
,不設置 expireAfterWrite
3.2 實踐2
配置方式:設置 maxSize
、expireAfterWrite
,不設置 refreshAfterWrite
存在問題:get 緩存間隔超過 expireAfterWrite
後,針對該 key,獲取到鎖的線程會同步執行 load,其餘未得到鎖的線程會阻塞等待,獲取鎖線程執行延時過長會致使其餘線程阻塞時間過長
適用場景:
-
緩存數據量大,限制緩存佔用的內存容量 -
緩存值會變,須要刷新緩存 -
不能夠接受緩存中存在舊數據 -
同步加載數據延遲小(使用 redis 等)
設置 maxSize
、expireAfterWrite
,不設置refreshAfterWrite
3.3 實踐3
配置方式:設置 maxSize
,不設置 refreshAfterWrite
、expireAfterWrite
,定時任務異步刷新數據
存在問題:須要手動定時任務異步刷新緩存
適用場景:
-
緩存數據量大,限制緩存佔用的內存容量 -
緩存值會變,須要刷新緩存 -
不能夠接受緩存中存在舊數據 -
同步加載數據延遲可能會很大
設置 maxSize,不設置 refreshAfterWrite
、expireAfterWrite
,定時任務異步刷新數據
3.4 實踐4
配置方式:設置 maxSize
、refreshAfterWrite
、expireAfterWrite
,refreshAfterWrite
< expireAfterWrite
存在問題:
-
get 緩存間隔在 refreshAfterWrite
和expireAfterWrite
之間,觸發緩存異步刷新,此時會獲取緩存中的舊值 -
get 緩存間隔大於 expireAfterWrite
,針對該 key,獲取到鎖的線程會同步執行 load,其餘未得到鎖的線程會阻塞等待,獲取鎖線程執行延時過長會致使其餘線程阻塞時間過長
適用場景:
-
緩存數據量大,限制緩存佔用的內存容量 -
緩存值會變,須要刷新緩存 -
能夠接受有限時間緩存中存在舊數據 -
同步加載數據延遲小(使用 redis 等)
設置 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源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。