1. 程式人生 > >你應該知道的Java快取進化史

你應該知道的Java快取進化史

本文主要講述愛奇藝的快取之路和本地快取的一個發展歷史,以及每一種快取的實現基本原理。
在這裡插入圖片描述

背景

本文是上週去技術沙龍聽了一下愛奇藝的 Java 快取之路有感寫出來的。先簡單介紹一下愛奇藝的 Java 快取道路的發展吧。
在這裡插入圖片描述
可以看見圖中分為幾個階段:

第一階段:資料同步加 Redis
歡迎工作一到五年的Java工程師朋友們加入Java技術交流:611481448
群內提供免費的Java架構學習資料(裡面有高可用、高併發、高效能及分散式、Jvm效能調優、Spring原始碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!

通過訊息佇列進行資料同步至 Redis,然後 Java 應用直接去取快取。這個階段的優點是:由於是使用的分散式快取,所以資料更新快。缺點也比較明顯:依賴 Redis 的穩定性,一旦 Redis 掛了,整個快取系統不可用,造成快取雪崩,所有請求打到 DB。

第二,三階段:JavaMap 到 Guava Cache
這個階段使用程序內快取作為一級快取,Redis 作為二級。優點:不受外部系統影響,其他系統掛了,依然能使用。缺點:程序內快取無法像分散式快取那樣做到實時更新。由於 Java 記憶體有限,必定快取得設定大小,然後有些快取會被淘汰,就會有命中率的問題。

第四階段: Guava Cache 重新整理


為了解決上面的問題,利用 Guava Cache 可以設定寫後重新整理時間,進行重新整理。解決了一直不更新的問題,但是依然沒有解決實時重新整理。

第五階段:外部快取非同步重新整理

在這裡插入圖片描述

這個階段擴充套件了 Guava Cache,利用 Redis 作為訊息佇列通知機制,通知其他 Java 應用程式進行重新整理。

這裡簡單介紹一下愛奇藝快取發展的五個階段,當然還有一些其他的優化,比如 GC 調優,快取穿透,快取覆蓋的一些優化等等。

原始社會 - 查庫

上面說的是愛奇藝的一個進化線路,但是在大家的一般開發過程中,第一步一般都沒有 Redis,而是直接查庫。

在流量不大的時候,查資料庫或者讀取檔案最為方便,也能完全滿足我們的業務要求。

古代社會 - HashMap

當我們應用有一定流量之後或者查詢資料庫特別頻繁,這個時候就可以祭出我們 Java 中自帶的 HashMap 或者 ConcurrentHashMap。我們可以在程式碼中這麼寫:

public class CustomerService { private HashMap < String ,
String

hashMap = new HashMap <>(); private CustomerMapper customerMapper; public String getCustomer( String name){
String customer = hashMap. get (name); if ( customer == null ){
customer = customerMapper. get (name); hashMap.put(name,customer); }
return customer; } }

但是這樣做就有個問題 HashMap 無法進行資料淘汰,記憶體會無限制的增長,所以 HashMap 很快也被淘汰了。

當然並不是說它完全就沒用,就像我們古代社會也不是所有的東西都是過時的,比如我們中華名族的傳統美德是永不過時的,就像這個 HashMap 一樣的可以在某些場景下作為快取,當不需要淘汰機制的時候,比如我們利用反射,如果我們每次都通過反射去搜索 Method,field,效能必定低效,這時我們用 HashMap 將其快取起來,效能能提升很多。

近代社會 - LRUHashMap

在古代社會中難住我們的問題是無法進行資料淘汰,這樣會導致我們記憶體無限膨脹,顯然我們是不可以接受的。

有人就說我把一些資料給淘汰掉唄,這樣不就對了,但是怎麼淘汰呢?隨機淘汰嗎?當然不行,試想一下你剛把 A 裝載進快取,下一次要訪問的時候就被淘汰了,那又會訪問我們的資料庫了,那我們要快取幹嘛呢?

所以聰明的人們就發明了幾種淘汰演算法,下面列舉下常見的三種 FIFO,LRU,LFU(還有一些 ARC,MRU 感興趣的可以自行搜尋):

FIFO:先進先出,在這種淘汰演算法中,先進入快取的會先被淘汰。

這種可謂是最簡單的了,但是會導致我們命中率很低。試想一下我們如果有個訪問頻率很高的資料是所有資料第一個訪問的,而那些不是很高的是後面再訪問的,那這樣就會把我們的首個數據但是他的訪問頻率很高給擠出。

LRU:最近最少使用演算法。
在這種演算法中避免了上面的問題,每次訪問資料都會將其放在我們的隊尾,如果需要淘汰資料,就只需要淘汰隊首即可。

但是這個依然有個問題,如果有個資料在 1 個小時的前 59 分鐘訪問了 1 萬次(可見這是個熱點資料),再後 1 分鐘沒有訪問這個資料,但是有其他的資料訪問,就導致了我們這個熱點資料被淘汰。

LFU:最近最少頻率使用。
在這種演算法中又對上面進行了優化,利用額外的空間記錄每個資料的使用頻率,然後選出頻率最低進行淘汰。這樣就避免了 LRU 不能處理時間段的問題。

上面列舉了三種淘汰策略,對於這三種,實現成本是一個比一個高,同樣的命中率也是一個比一個好。

而我們一般來說選擇的方案居中即可,即實現成本不是太高,而命中率也還行的 LRU,如何實現一個 LRUMap 呢?我們可以通過繼承 LinkedHashMap,重寫 removeEldestEntry 方法,即可完成一個簡單的 LRUMap。

class LRUMap extends LinkedHashMap { private final int
max; private Object lock ; public LRUMap ( int max, Object
lock ) { //無需擴容 super (( int ) (max *
1.4f ),
0.75f , true ); this .max = max; this . lock = lock ; } /** * 重寫LinkedHashMap的removeEldestEntry方法即可 * 在Put的時候判斷,如果為true,就會刪除最老的 * @param eldest * @return */ @Override protected boolean removeEldestEntry( Map . Entry eldest) { return size() > max; }
public Object getValue( Object key) { synchronized ( lock ) {
return get (key); } } public void putValue( Object key,
Object value) { synchronized ( lock ) { put(key, value); } }
public boolean removeValue( Object key) { synchronized ( lock )
{ return remove(key) != null ; } } public boolean
removeAll(){ clear(); return true ; } }

在 LinkedHashMap 中維護了一個 entry(用來放 key 和 value 的物件)連結串列。在每一次 get 或者 put 的時候都會把插入的新 entry,或查詢到的老 entry 放在我們連結串列末尾。

可以注意到我們在構造方法中,設定的大小特意設定到 max*1.4,在下面的 removeEldestEntry 方法中只需要 size>max 就淘汰,這樣我們這個 map 永遠也走不到擴容的邏輯了,通過重寫 LinkedHashMap,幾個簡單的方法我們實現了我們的 LruMap。

現代社會 - Guava Cache

在近代社會中已經發明出來了 LRUMap,用來進行快取資料的淘汰,但是有幾個問題:

鎖競爭嚴重,可以看見我的程式碼中,Lock 是全域性鎖,在方法級別上面的,當呼叫量較大時,效能必然會比較低。
不支援過期時間
不支援自動重新整理
所以谷歌的大佬們對於這些問題,按捺不住了,發明了 Guava Cache,在 Guava Cache 中你可以如下面的程式碼一樣,輕鬆使用:

public static void main( String [] args) throws
ExecutionException { LoadingCache < String , String

cache = CacheBuilder .newBuilder() .maximumSize( 100 ) //寫之後30ms過期 .expireAfterWrite( 30L , TimeUnit .MILLISECONDS)
//訪問之後30ms過期 .expireAfterAccess( 30L , TimeUnit .MILLISECONDS)
//20ms之後重新整理 .refreshAfterWrite( 20L , TimeUnit .MILLISECONDS)
//開啟weakKey key 當啟動垃圾回收時,該快取也被回收 .weakKeys()
.build(createCacheLoader()); System . out .println(cache. get (
“hello” )); cache.put( “hello1” , “我是hello1” ); System . out
.println(cache. get ( “hello1” )); cache.put( “hello1” , “我是hello2”
); System . out .println(cache. get ( “hello1” )); } public
static com.google.common.cache. CacheLoader < String , String
createCacheLoader() { return new com.google.common.cache. CacheLoader < String , String
() { @Override public String load( String key) throws Exception { return key; } }; }

我將會從 Guava Cache 原理中,解釋 Guava
Cache 是如何解決 LRUMap 的幾個問題的。

鎖競爭

Guava Cache 採用了類似 ConcurrentHashMap 的思想,分段加鎖,在每個段裡面各自負責自己的淘汰的事情。

在 Guava 根據一定的演算法進行分段,這裡要說明的是,如果段太少那競爭依然很嚴重,如果段太多容易出現隨機淘汰,比如大小為 100 的,給他分 100 個段,那也就是讓每個資料都獨佔一個段,而每個段會自己處理淘汰的過程,所以會出現隨機淘汰。在 Guava Cache 中通過如下程式碼,計算出應該如何分段。

int segmentShift = 0 ; int segmentCount = 1 ; while
(segmentCount < concurrencyLevel && (!evictsBySize() || segmentCount *
20 <= maxWeight)) { ++segmentShift; segmentCount <<= 1 ; }

上面 segmentCount 就是我們最後的分段數,其保證了每個段至少 10 個 entry。如果沒有設定 concurrencyLevel 這個引數,那麼預設就會是 4,最後分段數也最多為 4,例如我們 size 為 100,會分為 4 段,每段最大的 size 是 25。

在 Guava Cache 中對於寫操作直接加鎖,對於讀操作,如果讀取的資料沒有過期,且已經載入就緒,不需要進行加鎖,如果沒有讀到會再次加鎖進行二次讀,如果還沒有需要進行快取載入,也就是通過我們配置的 CacheLoader,我這裡配置的是直接返回 Key,在業務中通常配置從資料庫中查詢。 如下圖所示:

在這裡插入圖片描述

過期時間

相比於 LRUMap 多了兩種過期時間,一個是寫後多久過期 expireAfterWrite,一個是讀後多久過期 expireAfterAccess。

很有意思的事情是,在 Guava Cache 中對於過期的 entry 並沒有馬上過期(也就是並沒有後臺執行緒一直在掃),而是通過進行讀寫操作的時候進行過期處理,這樣做的好處是避免後臺執行緒掃描的時候進行全域性加鎖。看下面的程式碼:

public static void main( String [] args) throws
ExecutionException , InterruptedException { Cache < String ,
String

cache = CacheBuilder .newBuilder() .maximumSize( 100 ) //寫之後5s過期 .expireAfterWrite( 5 , TimeUnit .MILLISECONDS) .concurrencyLevel( 1
) .build(); cache.put( “hello1” , “我是hello1” ); cache.put(
“hello2” , “我是hello2” ); cache.put( “hello3” , “我是hello3” );
cache.put( “hello4” , “我是hello4” ); //至少睡眠5ms Thread .sleep( 5 );
System . out .println(cache.size()); cache.put( “hello5” ,
“我是hello5” ); System . out .println(cache.size()); } 輸出: 4 1

從這個結果中我們知道,在 put 的時候才進行的過期處理。特別注意的是我上面 concurrencyLevel(1)這裡將分段最大設定為 1,不然不會出現這個實驗效果的,在上面一節中已經說過,我們是以段位單位進行過期處理。在每個 Segment 中維護了兩個佇列:

final Queue < ReferenceEntry <K, V>> writeQueue; final Queue <
ReferenceEntry <K, V>> accessQueue;

writeQueue 維護了寫佇列,隊頭代表著寫得早的資料,隊尾代表寫得晚的資料。accessQueue 維護了訪問佇列,和 LRU 一樣,用來進行訪問時間的淘汰。如果當這個 Segment 超過最大容量,比如我們上面所說的 25,超過之後,就會把 accessQueue 這個佇列的第一個元素進行淘汰。

void expireEntries( long now) { drainRecencyQueue();
ReferenceEntry <K, V> e; while ((e = writeQueue.peek()) != null
&& map.isExpired(e, now)) { if (!removeEntry(e, e.getHash(),
RemovalCause .EXPIRED)) { throw new AssertionError (); } }
while ((e = accessQueue.peek()) != null && map.isExpired(e, now)) {
if (!removeEntry(e, e.getHash(), RemovalCause .EXPIRED)) { throw
new AssertionError (); } } }

上面就是 Guava Cache 處理過期 entries 的過程,會對兩個佇列一次進行 peek 操作,如果過期就進行刪除。

一般處理過期 entries 可以在我們的 put 操作的前後,或者讀取資料時發現過期了,然後進行整個 segment 的過期處理,又或者進行二次讀 lockedGetOrLoad 操作的時候呼叫。

void evictEntries( ReferenceEntry <K, V> newest) { ///… 省略無用程式碼
while (totalWeight > maxSegmentWeight) { ReferenceEntry <K, V> e =
getNextEvictable(); if (!removeEntry(e, e.getHash(), RemovalCause
.SIZE)) { throw new AssertionError (); } } } /**
**返回accessQueue的entry
**/ ReferenceEntry <K, V> getNextEvictable() { for ( ReferenceEntry <K, V> e : accessQueue) { int weight =
e.getValueReference().getWeight(); if (weight > 0 ) { return e;
} } throw new AssertionError (); }

上面是我們驅逐 entry 的時候的程式碼,可以看見訪問的是 accessQueue 對其隊頭進行驅逐。而驅逐策略一般是在對 segment 中的元素髮生變化時進行呼叫,比如插入操作,更新操作,載入資料操作。

自動重新整理

自動重新整理操作,在 Guava Cache 中實現相對比較簡單,直接通過查詢,判斷其是否滿足重新整理條件,進行重新整理。

其他特性

在 Guava Cache 中還有一些其他特性:

虛引用

在 Guava Cache 中,key 和 value 都能進行虛引用的設定,在 segment 中有兩個引用佇列:

final @Nullable ReferenceQueue keyReferenceQueue; final
@Nullable ReferenceQueue valueReferenceQueue;

這兩個佇列用來記錄被回收的引用,其中每個佇列記錄了每個被回收的 entry 的 hash,這樣回收了之後通過這個佇列中的 hash 值就能把以前的 entry 進行刪除。

刪除監聽器

在 Guava Cache 中,當有資料被淘汰時,但是你不知道他到底是過期,還是被驅逐,還是因為虛引用的物件被回收?

這個時候你可以呼叫這個方法 removalListener(RemovalListener listener)新增監聽器進行資料淘汰的監聽,可以打日誌或者一些其他處理,可以用來進行資料淘汰分析。

在 RemovalCause 記錄了所有被淘汰的原因:被使用者刪除,被使用者替代,過期,驅逐收集,由於大小淘汰。

Guava Cache 的總結

細細品讀 Guava Cache 的原始碼總結下來,其實就是一個性能不錯的,api 豐富的 LRU Map。愛奇藝的快取的發展也是基於此之上,通過對 Guava Cache 的二次開發,讓其可以進行 Java 應用服務之間的快取更新。

走向未來-caffeine

Guava Cache 的功能的確是很強大,滿足了絕大多數人的需求,但是其本質上還是 LRU 的一層封裝,所以在眾多其他較為優良的淘汰演算法中就相形見絀了。而 Caffeine Cache 實現了 W-TinyLFU(LFU+LRU 演算法的變種)。下面是不同演算法的命中率的比較:
在這裡插入圖片描述

其中 Optimal 是最理想的命中率,LRU 和其他演算法相比的確是個弟弟。而我們的 W-TinyLFU 是最接近理想命中率的。當然不僅僅是命中率 Caffeine 優於了 Guava Cache,在讀寫吞吐量上面也是完爆 Guava Cache。
在這裡插入圖片描述

這個時候你肯定會好奇為啥 Caffeine 這麼牛逼呢?彆著急下面慢慢給你道來。

W-TinyLFU

上面已經說過了傳統的 LFU 是怎麼一回事。在 LFU 中只要資料訪問模式的概率分佈隨時間保持不變時,其命中率就能變得非常高。

這裡我還是拿愛奇藝舉例,比如有部新劇出來了,我們使用 LFU 給他快取下來,這部新劇在這幾天大概訪問了幾億次,這個訪問頻率也在我們的 LFU 中記錄了幾億次。

但是新劇總會過氣的,比如一個月之後這個新劇的前幾集其實已經過氣了,但是他的訪問量的確是太高了,其他的電視劇根本無法淘汰這個新劇,所以在這種模式下是有侷限性。

所以各種 LFU 的變種出現了,基於時間週期進行衰減,或者在最近某個時間段內的頻率。同樣的 LFU 也會使用額外空間記錄每一個數據訪問的頻率,即使資料沒有在快取中也需要記錄,所以需要維護的額外空間很大。

可以試想我們對這個維護空間建立一個 HashMap,每個資料項都會存在這個 HashMap 中,當資料量特別大的時候,這個 HashMap 也會特別大。
再回到 LRU,我們的 LRU 也不是那麼一無是處,LRU 可以很好的應對突發流量的情況,因為他不需要累計資料頻率。

所以 W-TinyLFU 結合了 LRU 和 LFU,以及其他的演算法的一些特點。

頻率記錄

首先要說到的就是頻率記錄的問題,我們要實現的目標是利用有限的空間可以記錄隨時間變化的訪問頻率。在 W-TinyLFU 中使用 Count-Min Sketch 記錄我們的訪問頻率,而這個也是布隆過濾器的一種變種。如下圖所示::
在這裡插入圖片描述

如果需要記錄一個值,那我們需要通過多種 hash 演算法對其進行處理hash,然後在對應的 hash 演算法的記錄中+1,為什麼需要多種 hash 演算法呢?

由於這是一個壓縮演算法必定會出現衝突,比如我們建立一個 Long 的陣列,通過計算出每個資料的 hash 的位置。比如張三和李四,他們倆有可能 hash 值都是相同,比如都是 1 那 Long[1] 這個位置就會增加相應的頻率,張三訪問 1 萬次,李四訪問 1 次那 Long[1] 這個位置就是 1 萬零 1。

如果取李四的訪問評率的時候就會取出是 1 萬零 1,但是李四命名只訪問了 1 次啊,為了解決這個問題,所以用了多個 hash 演算法可以理解為 Long[][] 二維陣列的一個概念,比如在第一個演算法張三和李四衝突了,但是在第二個,第三個中很大的概率不衝突,比如一個演算法大概有 1% 的概率衝突,那四個演算法一起衝突的概率是 1% 的四次方。

通過這個模式,我們取李四的訪問率的時候取所有演算法中,李四訪問最低頻率的次數。所以他的名字叫 Count-Min Sketch。
在這裡插入圖片描述

這裡和以前的做個對比,簡單的舉個例子:如果一個 HashMap 來記錄這個頻率,如果我有 100 個數據,那這個 HashMap 就得儲存 100 個這個資料的訪問頻率。

哪怕我這個快取的容量是 1,因為 LFU 的規則我必須全部記錄這 100 個數據的訪問頻率。如果有更多的資料我就有記錄更多的。

在 Count-Min Sketch 中,我這裡直接說 Caffeine 中的實現吧(在 FrequencySketch 這個類中),如果你的快取大小是 100,他會生成一個 Long 陣列大小是和 100 最接近的 2 的冪的數,也就是 128。

而這個陣列將會記錄我們的訪問頻率。在 Caffeine 中它規則頻率最大為 15,15 的二進位制位 1111,總共是 4 位,而 Long 型是 64 位。所以每個 Long 型可以放 16 種演算法,但是 Caffeine 並沒有這麼做,只用了四種 hash 演算法,每個 Long 型被分為四段,每段裡面儲存的是四個演算法的頻率。

這樣做的好處是可以進一步減少 hash 衝突,原先 128 大小的 hash,就變成了 128X4。

一個Long的結構如下:
在這裡插入圖片描述

我們的 4 個段分為 A,B,C,D,在後面我也會這麼叫它們。而每個段裡面的四個演算法我叫他 s1,s2,s3,s4。下面舉個例子,如果要新增一個訪問 50 的數字頻率應該怎麼做?我們這裡用 size=100 來舉例。

首先確定 50 這個 hash 是在哪個段裡面,通過 hash & 3 必定能獲得小於 4 的數字,假設 hash & 3=0,那就在 A 段。
對 50 的 hash 再用其他 hash 演算法再做一次 hash,得到 Long 陣列的位置。假設用 s1 演算法得到 1,s2 演算法得到 3,s3 演算法得到 4,s4 演算法得到 0。
然後在 Long[1] 的 A 段裡面的 s1 位置進行+1,簡稱 1As1 加 1,然後在 3As2 加 1,在 4As3 加 1,在 0As4 加 1。
在這裡插入圖片描述

這個時候有人會質疑頻率最大為 15 的這個是否太小?沒關係在這個演算法中,比如 size 等於 100,如果他全域性提升了 1000 次就會全域性除以 2 衰減,衰減之後也可以繼續增加,這個演算法再 W-TinyLFU 的論文中證明了其可以較好的適應時間段的訪問頻率。

讀寫效能

在 Guava Cache 中我們說過其讀寫操作中夾雜著過期時間的處理,也就是你在一次 put 操作中有可能還會做淘汰操作,所以其讀寫效能會受到一定影響。

可以看上面的圖中,Caffeine 的確在讀寫操作上面完爆 Guava Cache。主要是因為在 Caffeine,對這些事件的操作是通過非同步操作,它將事件提交至佇列,這裡的佇列的資料結構是 RingBuffer。

然後會通過預設的 ForkJoinPool.commonPool(),或者自己配置執行緒池,進行取佇列操作,然後在進行後續的淘汰,過期操作。

當然讀寫也是有不同的佇列,在 Caffeine 中認為快取讀比寫多很多,所以對於寫操作是所有執行緒共享一個 Ringbuffer。
在這裡插入圖片描述

對於讀操作比寫操作更加頻繁,進一步減少競爭,其為每個執行緒配備了一個 RingBuffer:
在這裡插入圖片描述

資料淘汰策略

在 Caffeine 所有的資料都在 ConcurrentHashMap 中,這個和 Guava Cache 不同,Guava Cache 是自己實現了個類似 ConcurrentHashMap 的結構。在 Caffeine 中有三個記錄引用的 LRU 佇列:

Eden 佇列:在 Caffeine 中規定只能為快取容量的 %1,如果 size=100,那這個佇列的有效大小就等於 1。這個佇列中記錄的是新到的資料,防止突發流量由於之前沒有訪問頻率,而導致被淘汰。
比如有一部新劇上線,在最開始其實是沒有訪問頻率的,防止上線之後被其他快取淘汰出去,而加入這個區域。伊甸區,最舒服最安逸的區域,在這裡很難被其他資料淘汰。

Probation 佇列:叫做緩刑佇列,在這個佇列就代表你的資料相對比較冷,馬上就要被淘汰了。這個有效大小為 size 減去 eden 減去 protected。
Protected 佇列:在這個佇列中,可以稍微放心一下了,你暫時不會被淘汰,但是別急,如果 Probation 佇列沒有資料了或者 Protected 資料滿了,你也將會面臨淘汰的尷尬局面。
當然想要變成這個佇列,需要把 Probation 訪問一次之後,就會提升為 Protected 佇列。這個有效大小為(size 減去 eden) X 80% 如果 size =100,就會是 79。

這三個佇列關係如下:
在這裡插入圖片描述

所有的新資料都會進入 Eden。
Eden 滿了,淘汰進入 Probation。
如果在 Probation 中訪問了其中某個資料,則這個資料升級為 Protected。
如果 Protected 滿了又會繼續降級為 Probation。
對於發生資料淘汰的時候,會從 Probation 中進行淘汰,會把這個佇列中的資料隊頭稱為受害者,這個隊頭肯定是最早進入的,按照 LRU 佇列的演算法的話那它就應該被淘汰,但是在這裡只能叫它受害者,這個佇列是緩刑佇列,代表馬上要給它行刑了。

這裡會取出隊尾叫候選者,也叫攻擊者。這裡受害者會和攻擊者做 PK,通過我們的 Count-Min Sketch 中的記錄的頻率資料有以下幾個判斷:

如果攻擊者大於受害者,那麼受害者就直接被淘汰。
如果攻擊者<=5,那麼直接淘汰攻擊者。這個邏輯在他的註釋中有解釋: 他認為設定一個預熱的門檻會讓整體命中率更高。
在這裡插入圖片描述

其他情況,隨機淘汰。
如何使用

對於熟悉 Guava 的玩家來說,如果擔心有切換成本,那麼你就多慮了, Caffeine 的 api 借鑑了 Guava 的 api,可以發現其基本一模一樣。

public static void main( String [] args) { Cache < String ,
String

cache = Caffeine .newBuilder() .expireAfterWrite( 1 , TimeUnit .SECONDS) .expireAfterAccess( 1 , TimeUnit .SECONDS) .maximumSize(
10 ) .build(); cache.put( “hello” , “hello” ); }

順便一提的是,越來越多的開源框架都放棄了 Guava Cache,比如 Spring5。在業務上我也曾經比較過 Guava Cache 和 Caffeine,最終選擇了 Caffeine,在線上也有不錯的效果。所以不用擔心 Caffeine 不成熟,沒人使用。

最後

本文主要講述愛奇藝的快取之路和本地快取的一個發展歷史(從古至今到未來),以及每一種快取的實現基本原理。

喜歡小編輕輕點個關注吧!