一、前言
在項目開發中,為提升系統性能,減少 IO 開銷,本地緩存是必不可少的。最常見的本地緩存是 Guava 和 Caffeine,本篇文章將為大家介紹 Caffeine。
Caffeine 是基於 Google Guava Cache 設計經驗改進的結果,相較於 Guava 在性能和命中率上更具有效率,你可以認為其是 Guava Plus。
毋庸置疑的,你應該儘快將你的本地緩存從 Guava 遷移至 Caffeine,本文將重點和 Guava 對比二者性能佔據,給出本地緩存的最佳實踐,以及遷移策略。
二、PK Guava
2.1 功能

從功能上看,Guava 已經比較完善了,滿足了絕大部分本地緩存的需求。Caffine 除了提供 Guava 已有的功能外,同時還加入了一些擴展功能。
2.2 性能
Guava 中其讀寫操作夾雜着過期時間的處理,也就是你在一次 put 操作中有可能會做淘汰操作,所以其讀寫性能會受到一定影響。
Caffeine 在讀寫操作方面完爆 Guava,主要是因為 Caffeine 對這些事件的操作是異步的,將事件提交至隊列(使用 Disruptor RingBuffer),然後會通過默認的 ForkJoinPool.commonPool(),或自己配置的線程池,進行取隊列操作,然後再進行後續的淘汰、過期操作。
以下性能對比來自 Caffeine 官方提供數據:
(1)在此基準測試中,從配置了最大大小的緩存中,8 個線程並發讀:

(2)在此基準測試中,從配置了最大大小的緩存中,6個線程並發讀、2個線程並發寫:
image.png
(3)在此基準測試中,從配置了最大大小的緩存中,8 個線程並發寫:

image.png
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)時才會被替換。流程如下:
tiny-lfu-arch
儘管維護的是近期的訪問記錄,但仍然是非常昂貴的,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 算法作者,更多場景的性能測試參見官網:
database
W-TinyLFU 算法是對 TinyLFU算法的優化,能夠很好地解決一些稀疏的突發訪問元素。在一些數目很少但突發訪問量很大的場景下,TinyLFU將無法保存這類元素,因為它們無法在短時間內積累到足夠高的頻率,從而被過濾器過濾掉。W-TinyLFU 將新記錄暫時放入 Window Cache 裏面,只有通過 TinLFU 考察才能進入 Main Cache。大致流程如下圖:
W-TinyLFU
三、最佳實踐
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,定時任務異步刷新數據
存在問題:需要手動定時任務異步刷新緩存
適用場景:緩存數據量大,限制緩存佔用的內存容量緩存值會變,需要刷新緩存不可以接受緩存中存在舊數據同步加載數據延遲可能會很大
g
設置 maxSize,不設置 refreshAfterWrite、expireAfterWrite,定時任務異步刷新數據
3.4 實踐4
配置方式:設置 maxSize、refreshAfterWrite、expireAfterWrite,refreshAfterWrite < expireAfterWrite
存在問題:get 緩存間隔在 refreshAfterWrite 和 expireAfterWrite 之間,觸發緩存異步刷新,此時會獲取緩存中的舊值get 緩存間隔大於 expireAfterWrite,針對該 key,獲取到鎖的線程會同步執行 load,其他未獲得鎖的線程會阻塞等待,獲取鎖線程執行延時過長會導致其他線程阻塞時間過長
適用場景:緩存數據量大,限制緩存佔用的內存容量緩存值會變,需要刷新緩存可以接受有限時間緩存中存在舊數據同步加載數據延遲小(使用 redis 等)

設置 maxSize、refreshAfterWrite、expireAfterWrite
四、遷移指南
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()方法,因此需要做好判空處理。
原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-hk/n/224654.html
微信掃一掃
支付寶掃一掃