Caffeine緩存框架
本篇博文涉及技術點:
-
FIFO、LRU、LFU、Guava
-
java引用
- 強引用(非垃圾不會被清除)
- 軟引用SoftReference(內存不足時清除)
- 弱引用WeakReference(gc時刪除)
- 虛引用PlatformQueue
-
W-TinyLFU算法(window cache、SLRU、TinyLFU)、Count-Min Sketch算法、布隆過濾器
-
時間輪(TimerWheel)算法,多層時間輪(hierarchical timing wheels )算法
-
編程思想:
- 數據庫系統undolog、redolog的 WAL(Write-Ahead Logging)思想,即先寫日志再執行操作
- MPSC(Multiple Producer / Single Consumer)思想多生產者,單個消費者
-
消除偽共享
學習這個Caffeine緩存框架還是很有意思的,可以同步學習到的知識點很多
1,概述
我們為了加快訪問速度、提供性能,通常會使用到很多緩存機制,例如:mybatis一級緩存機制,MySQL自己的持久化緩存機制等等。
我們通常使用的redis也可以看做一種緩存,它可以大大的提高我們的訪問並發性,但是在分布式系統中由於網絡不可靠問題,我們也不能全部依賴redis緩存,為了進一步加快速度,我們還可以使用本地緩存。例如之前google的guava緩存,以及今天要着重介紹的caffeine緩存。
2,各種緩存綜述
2.1, FIFO
Fist in first out 先進先出:最先進入的緩存被最先淘汰掉,這個基本不會有人用來做緩存
2.2,LRU
Least recently used 最近最少未使用:每次訪問就把這個元素放到隊列頭部,隊列滿了淘汰隊列尾的元素,也就是淘汰最長時間沒有被訪問的。
在HashMap的鏈式法增加新的引用形成一個雙向鏈表,即是一個HashMap又是一個鏈表,這樣輸出即有序,也可以根據訪問來動態調整順序,HashMap+LinkedList(java容器中的LinkedHashMap就可以直接實現改功能)。
達到FIFO或者LRU的特點,可以明顯看出這個存在的問題,線程不安全,需要額外加鎖,功能結構單一,沒有過期時間容易存在內存泄露。
缺點也是很明顯的,某一時刻大量數據的到來容易把熱點數據擠出緩存,而這些數據卻是只訪問了一次的,今后不會再訪問了的或者訪問頻率極低的
2.3,LFU
Least frequently used 最不經常使用:也就是淘汰一定時期內被訪問次數最少的頁,這個和LRU區別是這個講究的是一定時期中的次數也就是頻率最低的被淘汰。
這個能避免LRU的缺點,因為是根據頻率淘汰,不會出現大量進進來的擠壓掉老的,如果在數據的訪問的模式不隨時間變化時候,LFU將會提供絕佳的命中率。但是如果訪問模式隨着時間而變化(即緩存元素隨着時間增大訪問次數越小),新進來的被快速淘汰,因為剛剛進來的頻率最低,之前老緩存的頻率太高。並且它需要額外空間維護頻率這個屬性,如果建立一個HashMap維護這個屬性,當數據量大的情況下,那么這個HashMap也會十分大。
2.4,Guava
Guava是google公司開發的一款Java類庫擴展工具包,內含了豐富的API,涵蓋了集合、緩存、並發、I/O等多個方面。使用這些API一方面可以簡化我們代碼,使代碼更為優雅,另一方面它補充了很多jdk中沒有的功能,能讓我們開發中更為高效。
在平常開發過程中,很多情況需要使用緩存來避免頻繁SQL查詢或者其他耗時操作,會采取緩存這些操作結果給下一次請求使用。如果我們的操作結果是一直不改變的,其實我們可以使用 ConcurrentHashMap 來存儲這些數據;但是如果這些結果在隨后時間內會改變或者我們希望存放的數據所占用的內存空間可控,這樣就需要自己來實現這種數據結構了。
缺點:
- 使用谷歌提供的ConcurrentLinkedHashMap有個漏洞,那就是緩存的過期只會發生在緩存達到上限的情況,否則便只會一直放在緩存中。咋一看,這個機制沒問題,是沒問題,可是卻不合理,舉個例子,有玩家上線后加載了一堆的數據放在緩存中,之后便不再上線了,那么這份緩存便會一直存在,知道緩存達到上限。(缺點:浪費內存)
- ConcurrentLinkedHashMap沒有提供基於時間淘汰時間的機制;
3,Caffeine
3.1,性能優勢:
性能優勢對比:
- Caffeine支持異步加載方式,直接返回CompletableFutures,相對於GuavaCache的同步方式,它不用阻塞等待數據的載入;
- GuavaCache是基於LRU的,而Caffeine是基於LRU和LFU的(W-TinyLFU算法),結合了兩者的優點;
- Caffeine另外一個比較快的原因,就是很多操作都使用了異步操作,把這些事件提交到隊列里。隊列使用的RingBuffer;
- 目前Spring也在推薦使用,caffeine在springboot2.0開始替代guava
3.2,結構
- Cache的內部包含着一個ConcurrentHashMap,這也是存放我們所有緩存數據的地方,眾所周知,ConcurrentHashMap是一個並發安全的容器,這點很重要,可以說Caffeine其實就是一個被強化過的ConcurrentHashMap;
- Scheduler(定時器),定期清空數據的一個機制,可以不設置,如果不設置則不會主動的清空過期數據;
- Executor,指定運行異步任務時要使用的線程池。可以不設置,如果不設置則會使用默認的線程池,也就是ForkJoinPool.commonPool();
3.3,實現原理
caffeine底層架構圖
3.3.1,執行流程
- 通過put操作將數據放入data屬性中(ConcurrentHashMap)
- 創建AddTask任務,放入(offer)寫緩存:writeBuffer
- 從writeBuffer中獲取任務,並執行其run方法,追加記錄頻率:frequencySketch().increment(key)
- 往window區寫入數據
- 如果數據超過window區大小,將數據移到probation區
- 比較從window區晉升的數據和probation區的老數據的頻率,輸者被淘汰,從data中刪除
3.3.2,cache內部結構
它包含如下的幾個結構:
- TinyLFU模塊,用於估算統計各個key值的請求頻率
- SLRU(Segmented LRU,即分段 LRU)、包括一個名為 protected 和一個名為 probation 的緩存區,通過增加一個緩存區(即 Window Cache),當有新的記錄插入時,會先在 window 區呆一下,就可以避免上述說的 sparse bursts 問題。
- window cache新手保護緩存(其本質就是一個LRU緩存)
上述結構結合就組成了我們下面要介紹的W-TinyLFU算法的核心部件!
3.3.3,W-TinyLFU算法
caffeine內部的核心算法是 W-TinyLFU,它是LFU的優化版本。
先說一下LFU的兩個缺點:
- 需要給每個記錄項維護頻率信息,每次訪問都需要更新,這是個巨大的開銷
- 對突發性的稀疏流量響應遲鈍,因為歷史的數據已經積累了很多次計數,新來的數據肯定是排在后續的
上述2個的問題解決辦法
1.針對第一個問題W-TinyLFU 采用了 Count–Min Sketch算法
Count–Min Sketch算法是一個頻率估計算法,它思想類似於Bloom Filter布隆過濾器,只不過在布隆過濾器的基礎上額外增加了一個計數的操作,它的流程如下
①選定d個hash函數,開一個 d & m 的二維整數數組作為哈希表
②對於每個元素,分別使用d個hash函數計算相應的哈希值,並對m取余,然后在對應的二維數組位置上增1,二維數組中的每個整數稱為sketch
③要查詢某個元素的頻率時,只需要取出d個sketch, 返回最小的那一個(其實d個sketch都是該元素的近似頻率,返回任意一個都可以,該算法選擇最小的那個)
Count–Min Sketch算法原理圖
Count–Min Sketch算法復雜度情況
- 空間復雜度
O(dm)
。Count-Min Sketch 需要開一個dxm
大小的二位數組,所以空間復雜度是O(dm)
- 時間復雜度
O(n)
。Count-Min Sketch 只需要一遍掃描,所以時間復雜度是O(n)
Count–Min Sketch算法優點:
- Caffeine認為統計頻率達到 15 次的頻率算是很高的了,那么只需要 4 個 bit 就可以滿足數據統計,一個 long 有 64bit,可以存儲 16 個這樣的統計數,Caffeine 就是這樣的設計,使得存儲效率提高了 16 倍(節省內存)
Count–Min Sketch算法缺點:
- 對於出現次數比較少的元素,准確性很差,因為二維數組相比於原始數據來說還是太小,hash沖突比較嚴重,導致結果偏差比較大(很明顯CM sketch對元素的頻率只會高估而不會低估,且對於重復次數較多的元素的准確率比較高,但是對於出現次數較少的元素的准確率較低)
臨時拓展知識:
1,這個Count–Min Sketch算法可以替代布隆過濾器,解決布隆過濾器不能刪除記錄的缺陷
2,對於頻率統計算法,有如下幾種思路:
①直接使用hashMap<Key值,count值>這種進行統計,缺點就是消耗堆內存;
②分片+hashMap其本質就和數據庫的水平拆分一樣;
③還有就是剛才的Count–Min Sketch算法;
2.針對第二個問題,解決辦法是讓記錄盡量保持相對的“新鮮”(Freshness Mechanism)
① caffeine增加一個新手保護緩存區(即 Window Cache)來存儲最新的數據(暫時待一下,新手保護機制),等其建立足夠的頻率,避免稀疏流量問題
② 當有新的記錄插入時,可以讓它跟老的記錄進行“PK”,輸者就會被淘汰,這樣一些老的、不再需要的記錄就會被剔除
3.3.4,淘汰策略
當 新增數據時,window cache 區滿(上圖中的LRU):
- 就會根據 LRU 把 candidate(即淘汰出來的元素)放到 probation 區
- 如果 probation 區也滿了,就把 candidate 和 probation 將要淘汰的元素 victim,兩個進行“PK”,勝者留在 probation,輸者就要被淘汰了。
而且經過實驗發現當 window 區配置為總容量的 1%,剩余的 99%當中的 80%分給 protected 區,20%分給 probation 區時,這時整體性能和命中率表現得最好,所以 Caffeine 默認的比例設置就是這個。
不過這個比例 Caffeine 會在運行時根據統計數據(statistics)去動態調整,
- 如果你的應用程序的緩存隨着時間變化比較快的話,那么增加 window Cache區(新手保護區)的比例可以提高命中率;
- 相反緩存都是比較固定不變的話,增加 Main Cache 區(本質就是SLRU區,protected 區 +probation 區)的比例會有較好的效果。
3.3.5,異步讀寫策略
一般的緩存每次對數據處理完之后(讀的話,已經存在則直接返回,不存在則 load 數據,保存,再返回;寫的話,則直接插入或更新,同步操作),但是因為要維護一些淘汰策略,則需要一些額外的操作,諸如:
- 計算和比較數據的是否過期
- 統計頻率(像 LFU 或其變種)
- 維護 read queue 和 write queue
- 淘汰符合條件的數據
- 等等
以前的Guava針對上述操作使用的策略是利用JDK自帶的ConcurrentHashMap(分段鎖或者無鎖CAS)來降低鎖的粒度,達到高並發的目的。但是,對於一些熱點數據(並發量比較高)還是避免不了頻繁的鎖競爭。
Caffeine借鑒了數據庫系統中的WAL(Write-Ahead Logging)思想,即先寫日志再執行操作,這種思想同樣適合緩存的,執行讀寫操作時,先把操作記錄在緩沖區,然后在合適的時機異步、批量地執行緩沖區中的內容。
3.3.6,過期策略
除了支持expireAfterAccess
和expireAfterWrite
之外(Guava Cache 也支持這兩個特性),Caffeine 還支持expireAfter
(本質就是自定義過期時間)。因為expireAfterAccess
和expireAfterWrite
都只能是固定的過期時間,這可能滿足不了某些場景,譬如記錄的過期時間是需要根據某些條件而不一樣的,這就需要用戶自定義過期時間。
而當使用了expireAfter
特性后,Caffeine 會啟用一種叫“時間輪”的算法來實現這個功能。
拓展知識:分層時間輪算法
分層時間輪算法是為了更高效的實現定時器而設計的一種數據格式,像 Netty 、ZooKeepr、Dubbo 這樣的開源項目都有使用到時間輪的實現,其中kafka更進一步使用的是分層時間輪算法。
定時器的核心需求
- 新增(初始化一個定時任務)
- 移除(過期任務)
- 任務到期檢測
定時器迭代歷史
1,鏈表實現的定時器
直接在一個鏈表中加入一個定時任務節點,每隔一個最小時間單位,開始從頭向尾部檢測,並將任務節點中的倒計時-1
- 如果倒計時變為0,那么說明該定時任務已經到期,就直接觸發它的執行操作,並將它從鏈表中刪除
- 如果倒計時還不為0,那么就繼續往尾部遍歷
時間復雜度:新增O(1),移除O(N),檢測O(N)
缺點:時間復雜度高
2,排序鏈表實現的定時器
還是一個鏈表的數據格式,但是它這個是將各個定時任務的執行時間做了一個排序,然后每個最小時間間隔檢測頭節點
- 如果頭結點的執行時間與當前時間一致,那么就開始執行該定時任務操作,並將頭節點移動到next節點,同時也檢測一下next節點
- 如果頭節點的執行時間與當前時間不一致,那么就等待下一個時間節點再次檢測
時間復雜度:新增O(N)需要額外排序操作,移除O(N),檢測O(1)只用檢測頭結點
如果使用最小堆新增和移除的時間復雜度都為O(logN)
缺點:時間復雜度高
3,普通時間輪實現的定時器
時間輪的本質就是一個數組,它的長度就是一個時間循環
以上圖為例,該時間輪的時間循環周期為8個最小時間間隔,時鍾輪詢從0>8>0~>8開始每一個最小時間間隔步進一個單位,然后檢查當前時間輪節點上是否有任務
- 如果有任務,就直接執行
- 沒有任務就等待下一個時間間隔步進1重復進行檢測
同時,它原版的會維護一個溢出列表(overflow list有序),因為定時任務有可能沒有在這個時間周期內,那么就將這些未來需要執行的任務放在溢出列表中,每次時鍾輪詢的時候,檢測一下是否可以添加到時間輪上
時間復雜度:純粹的時間輪-新增O(1),移除O(1),檢測O(1)
但是維護溢出列表需要額外資源,時間復雜度O(N)
缺點:時間復雜度高
4,分層時間輪實現定時器
本質就是多個時間輪共同一起作用,分時間層級!
以上述圖片為樣例,當前時間為2時59分1秒,新建一個3時0分2秒的定時任務,先將定時任務存儲在小時單位的時間輪上,存放位置為3時;
然后分層時間輪以秒進行驅動步進,秒驅動到59向0切換時,分鍾時間輪也隨之步進1,同理小時時間輪;
如果小時時間輪步進到3時,發現該節點上有一個定時任務,那么就將該任務轉移到對應的分鍾時間輪上,存放位置為0分;同理如果分鍾時間輪發現當前的節點中有定時任務,那么就將其轉移到秒時間輪上,存放位置為1;秒時間輪發現當前節點有任務,那么就直接執行!
時間復雜度:新增O(1),移除O(1),檢測O(1)
3.3.7,消除偽共享
拓展知識:偽共享
1,什么是偽共享
CPU 緩存系統中是以緩存行(cache line)為單位存儲的。目前主流的 CPU Cache 的 Cache Line 大小都是 64 Bytes。在多線程情況下,如果需要修改“共享同一個緩存行的變量”,就會無意中影響彼此的性能,這就是偽共享(False Sharing)。
本質就是在內存中,每個最小存儲單元是64 字節為單位的塊(chunk)進行存取,例如需要緩存long類型數據(8字節),那么就是一個最小內存單元中存儲8個long類型數據成為一個chunk,如果我這邊有8個線程並發分別修改這一個chunk中的8個long類型數據,就會出現性能影響的問題,必須是一個修改完另外一個再重新載入修改!否則,就會出現並發數據覆蓋問題。
2,cpu的三級緩存
一、二級緩存屬於各核心獨享,而三級緩存是核心共享的,所有三級緩存的容量相對於一二級緩存要大得多
由於 CPU 的速度遠遠大於內存速度,所以 CPU 設計者們就給 CPU 加上了緩存(CPU Cache)。 以免運算被內存速度拖累。(就像我們寫代碼把共享數據做Cache不想被DB存取速度拖累一樣),CPU Cache 分成了三個級別:L1,L2,L3。越靠近CPU的緩存越快也越小。L1 緩存很小但很快,並且緊靠着在使用它的 CPU 內核。L2 大一些,也慢一些,並且仍然只能被一個單獨的 CPU 核使用。L3 在現代多核機器中更普遍,仍然更大,更慢,並且被單個插槽上的所有 CPU 核共享。最后,你擁有一塊主存,由全部插槽上的所有 CPU 核共享。
當 CPU 執行運算的時候,它先去L1查找所需的數據,再去L2,然后是L3,最后如果這些緩存中都沒有,所需的數據就要去主內存拿。走得越遠,運算耗費的時間就越長。所以如果你在做一些很頻繁的事,你要確保數據在L1緩存中。
3.4,特性
Caffeine提供了多種靈活的構造方法,從而可以創建多種特性的本地緩存。
- 自動把數據加載到本地緩存中,並且可以配置異步;
- 基於數量剔除策略;
- 基於失效時間剔除策略,這個時間是從最后一次操作算起【訪問或者寫入】;
- 異步刷新;
- Key會被包裝成Weak引用;
- Value會被包裝成Weak或者Soft引用,從而能被GC掉,而不至於內存泄漏;
- 數據剔除提醒;
- 寫入廣播機制;
- 緩存訪問可以統計;
3種加載方式
- 手動加載 cache.put(key1, value1);
- 同步加載 cache.get(key1) --> load(key1)
- 異步加載 cache.get(key1) --> CompletableFuture.supplyAsync(() -> {return oldValue;},executorService);
4種淘汰機制
- 基於大小
- 設置方式:maximumSize(個數),這意味着當緩存大小超過配置的大小限制時會發生回收
- 基於權重
- 設置方式:maximumWeight(個數),意味着當緩存大小超過配置的權重限制時會發生回收
- 例如設置最大權重為2,權重的計算方式是直接用key,當put 1 進來時總權重為1,當put 2 進緩存是總權重為3,超過最大權重2,因此會觸發淘汰機制,回收后個數只為1
- 基於時間
- 訪問后到期,時間節點從最近一次讀或者寫,也就是get或者put開始算起。
- 寫入后到期,時間節點從寫開始算起,也就是put。
- 自定義策略,自定義具體到期時間。
- 基於引用
- .weakKeys() // 設置Key為弱引用,生命周期是下次gc的時候
- .weakValues() // 設置value為弱引用,生命周期是下次gc的時候
目前數據被淘汰的原因不外有以下幾個:
- EXPLICIT:如果原因是這個,那么意味着數據被我們手動的remove掉了。
- REPLACED:就是替換了,也就是put數據的時候舊的數據被覆蓋導致的移除。
- COLLECTED:這個有歧義點,其實就是收集,也就是垃圾回收導致的,一般是用弱引用或者軟引用會導致這個情況。
- EXPIRED:數據過期,無需解釋的原因。
- SIZE:個數超過限制導致的移除。
4,使用
4.1,java樣例
4.1.1,邏輯流程圖
4.1.2,maven依賴
<!-- caffeine緩存框架 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.8.8</version>
</dependency>
4.1.3,java代碼
模擬memoryCache、redis二級緩存
package com.springcloud.test;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import java.util.HashMap;
import java.util.concurrent.*;
/**
* @author zjk
* @date 2022/3/11
* @descript
* @since V1.0.0
*/
public class CaffeineTest {
// 模擬redis緩存
private static HashMap<String, String> redisLike = new HashMap<>();
static {
redisLike.put("k1","v1");
redisLike.put("k2","v2");
}
// 模擬數據庫
private static HashMap<String, String> dbLike = new HashMap<>();
static {
dbLike.put("k1", "v1");
dbLike.put("k2", "v2");
dbLike.put("k3", "v3");
dbLike.put("k4", "v4");
}
public static String getValue(String key) {
return localCache.get(key);
}
private static ThreadPoolExecutor executorService = new ThreadPoolExecutor(2,
4,
2,
TimeUnit.MINUTES,
new LinkedBlockingDeque<>(), new ThreadFactoryBuilder().build());
// 注意,注意 這里為了方便模擬實際使用過程中可能遇到的情況,參數設置比較極端
private static LoadingCache<String, String> localCache = Caffeine.newBuilder()
.maximumSize(1) // 最大緩存容量個數
.expireAfterWrite(50, TimeUnit.SECONDS) // 過期時間
.refreshAfterWrite(1, TimeUnit.SECONDS) // 寫入后多久進行緩存刷新
.build(new CacheLoader<String, String>() {
@Nullable
@Override
public String load(@NonNull String token) {
// 模擬三級緩存刷入數據
String value = null;
if (redisLike.containsKey(token)) {
System.out.println("從redis中獲取數據");
value = redisLike.get(token);
} else if (dbLike.containsKey(token)) {
System.out.println("從db中獲取數據");
value = dbLike.get(token);
redisLike.put(token, value); // 需要給redis緩存設置過期時間,防止內存滿了
} else {
System.out.printf("獲取到%s的數據為空%n", token);
}
return value;
}
@Override
public @NonNull
CompletableFuture<String> asyncReload(@NonNull String key, @NonNull String oldValue, @NonNull Executor executor) {
// System.out.println("自動刷新緩存,key:" + key +",value:"+ oldValue);
return CompletableFuture.supplyAsync(() -> {
// System.out.println("執行異步刷新");
return oldValue;
},executorService);
}
});
public static void main(String[] args) throws InterruptedException {
System.out.println("value:"+localCache.get("k0"));
Thread.sleep(300);
System.out.println("value:"+localCache.get("k0"));
dbLike.put("k0","v0"); // 模擬中途刷新數據
Thread.sleep(300);
System.out.println("value:"+localCache.get("k0"));
System.out.println("value:"+localCache.get("k1"));
Thread.sleep(300);
System.out.println("value:"+localCache.get("k1"));
Thread.sleep(300);
System.out.println("value:"+localCache.get("k1"));
System.out.println("value:"+localCache.get("k3"));
Thread.sleep(1000);
System.out.println("value:"+localCache.get("k3"));
Thread.sleep(1000);
System.out.println("value:"+localCache.get("k3"));
localCache.refresh("k4");// 模擬主動刷新數據
System.out.println("主動刷新數據k4數據");
System.out.println("value:"+localCache.get("k4"));
localCache.put("k4","v004");
System.out.println("主動刷新數據k4數據");
Thread.sleep(1000);
System.out.println("value:"+localCache.get("k4"));
Thread.sleep(1000);
System.out.println("value:"+localCache.get("k4"));
Thread.sleep(2000);// k0過期后重新獲取
System.out.println("value:"+localCache.get("k0"));
}
}
執行結果:
獲取到k0的數據為空
value:null
獲取到k0的數據為空
value:null
從db中獲取數據
value:v0
從redis中獲取數據
value:v1
value:v1
value:v1
從db中獲取數據
value:v3
value:v3
value:v3
從db中獲取數據
主動刷新數據k4數據
value:v4
主動刷新數據k4數據
value:v004
value:v004
從redis中獲取數據
value:v0
4.1.4,結果分析
由上述代碼可以完美的模擬(memoryCache、redis、db三級緩存)三級緩存操作,先去memoryCache中查詢數據,沒有再去redis中查詢數據(有就直接返回),如果還沒有(就去db里面查詢,如果有就刷新redis並直接返回),如果還沒有就直接返回null。
分析獲取k0:
獲取到k0的數據為空
value:null
獲取到k0的數據為空
value:null
從db中獲取數據
value:v0
....(最后一段邏輯獲取k0)
從redis中獲取數據
value:v0
// 由於redis和db中都沒有目標數據,所以沒有刷新數據直接返回空
// 在某一時刻,db中刷入數據,獲取到數據直接刷新至memoryCache和redis
// 最后一段邏輯獲取k0,可以直接在redis中獲取k0數據
分析獲取k1、k3:由於redis、db中有相關數據所以直接刷新至memoryCache即可
分析獲取k4:主動預熱數據操作、刷新k4-v004
實際使用過程中的配置(根據實際情況修改):
Caffeine.newBuilder()
.maximumSize(1000) // 最大緩存容量個數
.expireAfterWrite(10, TimeUnit.MINUTES) // 過期時間,10分鍾后過期
.refreshAfterWrite(1, TimeUnit.SECONDS) // 寫入后多久進行緩存刷新
.build();
4.2,結合Spring使用
4.3,從Guava遷移
參考鏈接
- 為什么Caffeine比Guava好?
- Caffeine與Guava對比
- caffeine配置及注意事項
- CacheManager與配置文件
- SpringBoot+SpringCache實現兩級緩存(Redis+Caffeine)
- Guava cacha 機制及源碼分析
- 全網最權威的Caffeine教程
- Caffeine的Window TinyLfu算法分析
- java引用
- 聊聊MyBatis緩存機制
- SpringBoot + Caffeine本地緩存
- 從 Kafka 看時間輪算法設計
- 【EP02】超級好玩的數據結構:定時器,時間輪,分層時間輪 Timer, Timing Wheel, Hierarchical Timing Wheel
- Java 中的偽共享詳解及解決方案
- Caffaine-github