摘要:先簡單介紹一下愛奇藝的緩存道路的發(fā)展吧。可以看見圖中分為幾個階段第一階段數(shù)據(jù)同步加通過消息隊列進(jìn)行數(shù)據(jù)同步至,然后應(yīng)用直接去取緩存這個階段優(yōu)點(diǎn)是由于是使用的分布式緩存,所以數(shù)據(jù)更新快。愛奇藝的緩存的發(fā)展也是基于此之上,通過對的二次開發(fā)
1.背景
本文是上周去技術(shù)沙龍聽了一下愛奇藝的Java緩存之路有感寫出來的。先簡單介紹一下愛奇藝的java緩存道路的發(fā)展吧。
可以看見圖中分為幾個階段:
第一階段:數(shù)據(jù)同步加redis
通過消息隊列進(jìn)行數(shù)據(jù)同步至redis,然后Java應(yīng)用直接去取緩存
這個階段優(yōu)點(diǎn)是:由于是使用的分布式緩存,所以數(shù)據(jù)更新快。缺點(diǎn)也比較明顯:依賴Redis的穩(wěn)定性,一旦redis掛了,整個緩存系統(tǒng)不可用,造成緩存雪崩,所有請求打到DB。
第二,三階段:JavaMap到Guava cache
這個階段使用進(jìn)程內(nèi)緩存作為一級緩存,redis作為二級。優(yōu)點(diǎn):不受外部系統(tǒng)影響,其他系統(tǒng)掛了,依然能使用。缺點(diǎn):進(jìn)程內(nèi)緩存無法像分布式緩存那樣做到實(shí)時更新。由于java內(nèi)存有限,必定緩存得設(shè)置大小,然后有些緩存會被淘汰,就會有命中率的問題。
第四階段: Guava Cache刷新
為了解決上面的問題,利用Guava Cache可以設(shè)置寫后刷新時間,進(jìn)行刷新。解決了一直不更新的問題,但是依然沒有解決實(shí)時刷新。
第五階段: 外部緩存異步刷新
這個階段擴(kuò)展了Guava Cache,利用redis作為消息隊列通知機(jī)制,通知其他java應(yīng)用程序進(jìn)行刷新。
這里簡單介紹一下愛奇藝緩存發(fā)展的五個階段,當(dāng)然還有一些其他的優(yōu)化,比如GC調(diào)優(yōu),緩存穿透,緩存覆蓋的一些優(yōu)化等等。有興趣的同學(xué)可以關(guān)注公眾號,聯(lián)系我進(jìn)行交流。
原始社會 - 查庫上面說的是愛奇藝的一個進(jìn)化線路,但是在大家的一般開發(fā)過程中,第一步一般都沒有redis,而是直接查庫。
在流量不大的時候,查數(shù)據(jù)庫或者讀取文件是最為方便,也能完全滿足我們的業(yè)務(wù)要求。
古代社會 - HashMap當(dāng)我們應(yīng)用有一定流量之后或者查詢數(shù)據(jù)庫特別頻繁,這個時候就可以祭出我們的java中自帶的HashMap或者ConcurrentHashMap。我們可以在代碼中這么寫:
public class CustomerService { private HashMaphashMap = 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無法進(jìn)行數(shù)據(jù)淘汰,內(nèi)存會無限制的增長,所以hashMap很快也被淘汰了。當(dāng)然并不是說他完全就沒用,就像我們古代社會也不是所有的東西都是過時的,比如我們中華名族的傳統(tǒng)美德是永不過時的,就像這個hashMap一樣的可以在某些場景下作為緩存,當(dāng)不需要淘汰機(jī)制的時候,比如我們利用反射,如果我們每次都通過反射去搜索Method,field,性能必定低效,這時我們用HashMap將其緩存起來,性能能提升很多。
近代社會 - LRUHashMap在古代社會中難住我們的問題無法進(jìn)行數(shù)據(jù)淘汰,這樣會導(dǎo)致我們內(nèi)存無限膨脹,顯然我們是不可以接受的。有人就說我把一些數(shù)據(jù)給淘汰掉唄,這樣不就對了,但是怎么淘汰呢?隨機(jī)淘汰嗎?當(dāng)然不行,試想一下你剛把A裝載進(jìn)緩存,下一次要訪問的時候就被淘汰了,那又會訪問我們的數(shù)據(jù)庫了,那我們要緩存干嘛呢?
所以聰明的人們就發(fā)明了幾種淘汰算法,下面列舉下常見的三種FIFO,LRU,LFU(還有一些ARC,MRU感興趣的可以自行搜索):
FIFO:先進(jìn)先出,在這種淘汰算法中,先進(jìn)入緩存的會先被淘汰。這種可謂是最簡單的了,但是會導(dǎo)致我們命中率很低。試想一下我們?nèi)绻袀€訪問頻率很高的數(shù)據(jù)是所有數(shù)據(jù)第一個訪問的,而那些不是很高的是后面再訪問的,那這樣就會把我們的首個數(shù)據(jù)但是他的訪問頻率很高給擠出。
LRU:最近最少使用算法。在這種算法中避免了上面的問題,每次訪問數(shù)據(jù)都會將其放在我們的隊尾,如果需要淘汰數(shù)據(jù),就只需要淘汰隊首即可。但是這個依然有個問題,如果有個數(shù)據(jù)在1個小時的前59分鐘訪問了1萬次(可見這是個熱點(diǎn)數(shù)據(jù)),再后一分鐘沒有訪問這個數(shù)據(jù),但是有其他的數(shù)據(jù)訪問,就導(dǎo)致了我們這個熱點(diǎn)數(shù)據(jù)被淘汰。
LFU:最近最少頻率使用。在這種算法中又對上面進(jìn)行了優(yōu)化,利用額外的空間記錄每個數(shù)據(jù)的使用頻率,然后選出頻率最低進(jìn)行淘汰。這樣就避免了LRU不能處理時間段的問題。
上面列舉了三種淘汰策略,對于這三種,實(shí)現(xiàn)成本是一個比一個高,同樣的命中率也是一個比一個好。而我們一般來說選擇的方案居中即可,即實(shí)現(xiàn)成本不是太高,而命中率也還行的LRU,如何實(shí)現(xiàn)一個LRUMap呢?我們可以通過繼承LinkedHashMap,重寫removeEldestEntry方法,即可完成一個簡單的LRUMap。
class LRUMap extends LinkedHashMap { private final int max; private Object lock; public LRUMap(int max, Object lock) { //無需擴(kuò)容 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中維護(hù)了一個entry(用來放key和value的對象)鏈表。在每一次get或者put的時候都會把插入的新entry,或查詢到的老entry放在我們鏈表末尾。
可以注意到我們在構(gòu)造方法中,設(shè)置的大小特意設(shè)置到max*1.4,在下面的removeEldestEntry方法中只需要size>max就淘汰,這樣我們這個map永遠(yuǎn)也走不到擴(kuò)容的邏輯了,通過重寫LinkedHashMap,幾個簡單的方法我們實(shí)現(xiàn)了我們的LruMap。
在近代社會中已經(jīng)發(fā)明出來了LRUMap,用來進(jìn)行緩存數(shù)據(jù)的淘汰,但是有幾個問題:
鎖競爭嚴(yán)重,可以看見我的代碼中,Lock是全局鎖,在方法級別上面的,當(dāng)調(diào)用量較大時,性能必然會比較低。
不支持過期時間
不支持自動刷新
所以谷歌的大佬們對于這些問題,按捺不住了,發(fā)明了Guava cache,在Guava cache中你可以如下面的代碼一樣,輕松使用:
public static void main(String[] args) throws ExecutionException { LoadingCachecache = CacheBuilder.newBuilder() .maximumSize(100) //寫之后30ms過期 .expireAfterWrite(30L, TimeUnit.MILLISECONDS) //訪問之后30ms過期 .expireAfterAccess(30L, TimeUnit.MILLISECONDS) //20ms之后刷新 .refreshAfterWrite(20L, TimeUnit.MILLISECONDS) //開啟weakKey key 當(dāng)啟動垃圾回收時,該緩存也被回收 .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 createCacheLoader() { return new com.google.common.cache.CacheLoader () { @Override public String load(String key) throws Exception { return key; } }; }
我將會從guava cache原理中,解釋guava cache是如何解決LRUMap的幾個問題的。
鎖競爭guava cache采用了類似ConcurrentHashMap的思想,分段加鎖,在每個段里面各自負(fù)責(zé)自己的淘汰的事情。在Guava根據(jù)一定的算法進(jìn)行分段,這里要說明的是,如果段太少那競爭依然很嚴(yán)重,如果段太多會容易出現(xiàn)隨機(jī)淘汰,比如大小為100的,給他分100個段,那也就是讓每個數(shù)據(jù)都獨(dú)占一個段,而每個段會自己處理淘汰的過程,所以會出現(xiàn)隨機(jī)淘汰。在guava cache中通過如下代碼,計算出應(yīng)該如何分段。
int segmentShift = 0; int segmentCount = 1; while (segmentCount < concurrencyLevel && (!evictsBySize() || segmentCount * 20 <= maxWeight)) { ++segmentShift; segmentCount <<= 1; }
上面segmentCount就是我們最后的分段數(shù),其保證了每個段至少10個Entry。如果沒有設(shè)置concurrencyLevel這個參數(shù),那么默認(rèn)就會是4,最后分段數(shù)也最多為4,例如我們size為100,會分為4段,每段最大的size是25。
在guava cache中對于寫操作直接加鎖,對于讀操作,如果讀取的數(shù)據(jù)沒有過期,且已經(jīng)加載就緒,不需要進(jìn)行加鎖,如果沒有讀到會再次加鎖進(jìn)行二次讀,如果還沒有需要進(jìn)行緩存加載,也就是通過我們配置的CacheLoader,我這里配置的是直接返回Key,在業(yè)務(wù)中通常配置從數(shù)據(jù)庫中查詢。
如下圖所示:
相比于LRUMap多了兩種過期時間,一個是寫后多久過期expireAfterWrite,一個是讀后多久過期expireAfterAccess。很有意思的事情是,在guava cache中對于過期的Entry并沒有馬上過期(也就是并沒有后臺線程一直在掃),而是通過進(jìn)行讀寫操作的時候進(jìn)行過期處理,這樣做的好處是避免后臺線程掃描的時候進(jìn)行全局加鎖。看下面的代碼:
public static void main(String[] args) throws ExecutionException, InterruptedException { Cachecache = 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
從這個結(jié)果中我們知道,在put的時候才進(jìn)行的過期處理。特別注意的是我上面concurrencyLevel(1)我這里將分段最大設(shè)置為1,不然不會出現(xiàn)這個實(shí)驗(yàn)效果的,在上面一節(jié)中已經(jīng)說過,我們是以段位單位進(jìn)行過期處理。在每個Segment中維護(hù)了兩個隊列:
final Queue> writeQueue; final Queue > accessQueue;
writeQueue維護(hù)了寫隊列,隊頭代表著寫得早的數(shù)據(jù),隊尾代表寫得晚的數(shù)據(jù)。
accessQueue維護(hù)了訪問隊列,和LRU一樣,用來我們進(jìn)行訪問時間的淘汰,如果當(dāng)這個Segment超過最大容量,比如我們上面所說的25,超過之后,就會把a(bǔ)ccessQueue這個隊列的第一個元素進(jìn)行淘汰。
void expireEntries(long now) { drainRecencyQueue(); ReferenceEntrye; 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的過程,會對兩個隊列一次進(jìn)行peek操作,如果過期就進(jìn)行刪除。一般處理過期Entries可以在我們的put操作的前后,或者讀取數(shù)據(jù)時發(fā)現(xiàn)過期了,然后進(jìn)行整個Segment的過期處理,又或者進(jìn)行二次讀lockedGetOrLoad操作的時候調(diào)用。
void evictEntries(ReferenceEntrynewest) { ///... 省略無用代碼 while (totalWeight > maxSegmentWeight) { ReferenceEntry e = getNextEvictable(); if (!removeEntry(e, e.getHash(), RemovalCause.SIZE)) { throw new AssertionError(); } } } /** **返回accessQueue的entry **/ ReferenceEntry getNextEvictable() { for (ReferenceEntry e : accessQueue) { int weight = e.getValueReference().getWeight(); if (weight > 0) { return e; } } throw new AssertionError(); }
上面是我們驅(qū)逐Entry的時候的代碼,可以看見訪問的是accessQueue對其隊頭進(jìn)行驅(qū)逐。而驅(qū)逐策略一般是在對segment中的元素發(fā)生變化時進(jìn)行調(diào)用,比如插入操作,更新操作,加載數(shù)據(jù)操作。
自動刷新自動刷新操作,在guava cache中實(shí)現(xiàn)相對比較簡單,直接通過查詢,判斷其是否滿足刷新條件,進(jìn)行刷新。
其他特性在Guava cache中還有一些其他特性:
虛引用在Guava cache中,key和value都能進(jìn)行虛引用的設(shè)定,在Segment中的有兩個引用隊列:
final @Nullable ReferenceQueuekeyReferenceQueue; final @Nullable ReferenceQueue valueReferenceQueue;
這兩個隊列用來記錄被回收的引用,其中每個隊列記錄了每個被回收的Entry的hash,這樣回收了之后通過這個隊列中的hash值就能把以前的Entry進(jìn)行刪除。
刪除監(jiān)聽器在guava cache中,當(dāng)有數(shù)據(jù)被淘汰時,但是你不知道他到底是過期,還是被驅(qū)逐,還是因?yàn)樘撘玫膶ο蟊换厥眨窟@個時候你可以調(diào)用這個方法removalListener(RemovalListener listener)添加監(jiān)聽器進(jìn)行數(shù)據(jù)淘汰的監(jiān)聽,可以打日志或者一些其他處理,可以用來進(jìn)行數(shù)據(jù)淘汰分析。
在RemovalCause記錄了所有被淘汰的原因:被用戶刪除,被用戶替代,過期,驅(qū)逐收集,由于大小淘汰。guava cache的總結(jié)
細(xì)細(xì)品讀guava cache的源碼總結(jié)下來,其實(shí)就是一個性能不錯的,api豐富的LRU Map。愛奇藝的緩存的發(fā)展也是基于此之上,通過對guava cache的二次開發(fā),讓其可以進(jìn)行java應(yīng)用服務(wù)之間的緩存更新。
走向未來-caffeineguava cache的功能的確是很強(qiáng)大,滿足了絕大多數(shù)的人的需求,但是其本質(zhì)上還是LRU的一層封裝,所以在眾多其他較為優(yōu)良的淘汰算法中就相形見絀了。而caffeine cache實(shí)現(xiàn)了W-TinyLFU(LFU+LRU算法的變種)。下面是不同算法的命中率的比較:
其中Optimal是最理想的命中率,LRU和其他算法相比的確是個弟弟。而我們的W-TinyLFU 是最接近理想命中率的。當(dāng)然不僅僅是命中率caffeine優(yōu)于了guava cache,在讀寫吞吐量上面也是完爆guava cache。
這個時候你肯定會好奇為啥這么caffeine這么牛逼呢?別著急下面慢慢給你道來。
W-TinyLFU上面已經(jīng)說過了傳統(tǒng)的LFU是怎么一回事。在LFU中只要數(shù)據(jù)訪問模式的概率分布隨時間保持不變時,其命中率就能變得非常高。這里我還是拿愛奇藝舉例,比如有部新劇出來了,我們使用LFU給他緩存下來,這部新劇在這幾天大概訪問了幾億次,這個訪問頻率也在我們的LFU中記錄了幾億次。但是新劇總會過氣的,比如一個月之后這個新劇的前幾集其實(shí)已經(jīng)過氣了,但是他的訪問量的確是太高了,其他的電視劇根本無法淘汰這個新劇,所以在這種模式下是有局限性。所以各種LFU的變種出現(xiàn)了,基于時間周期進(jìn)行衰減,或者在最近某個時間段內(nèi)的頻率。同樣的LFU也會使用額外空間記錄每一個數(shù)據(jù)訪問的頻率,即使數(shù)據(jù)沒有在緩存中也需要記錄,所以需要維護(hù)的額外空間很大。
可以試想我們對這個維護(hù)空間建立一個hashMap,每個數(shù)據(jù)項(xiàng)都會存在這個hashMap中,當(dāng)數(shù)據(jù)量特別大的時候,這個hashMap也會特別大。
再回到LRU,我們的LRU也不是那么一無是處,LRU可以很好的應(yīng)對突發(fā)流量的情況,因?yàn)樗恍枰塾嫈?shù)據(jù)頻率。
所以W-TinyLFU結(jié)合了LRU和LFU,以及其他的算法的一些特點(diǎn)。
頻率記錄首先要說到的就是頻率記錄的問題,我們要實(shí)現(xiàn)的目標(biāo)是利用有限的空間可以記錄隨時間變化的訪問頻率。在W-TinyLFU中使用Count-Min Sketch記錄我們的訪問頻率,而這個也是布隆過濾器的一種變種。如下圖所示:
如果需要記錄一個值,那我們需要通過多種Hash算法對其進(jìn)行處理hash,然后在對應(yīng)的hash算法的記錄中+1,為什么需要多種hash算法呢?由于這是一個壓縮算法必定會出現(xiàn)沖突,比如我們建立一個Long的數(shù)組,通過計算出每個數(shù)據(jù)的hash的位置。比如張三和李四,他們兩有可能hash值都是相同,比如都是1那Long[1]這個位置就會增加相應(yīng)的頻率,張三訪問1萬次,李四訪問1次那Long[1]這個位置就是1萬零1,如果取李四的訪問評率的時候就會取出是1萬零1,但是李四命名只訪問了1次啊,為了解決這個問題,所以用了多個hash算法可以理解為long[][]二維數(shù)組的一個概念,比如在第一個算法張三和李四沖突了,但是在第二個,第三個中很大的概率不沖突,比如一個算法大概有1%的概率沖突,那四個算法一起沖突的概率是1%的四次方。通過這個模式我們?nèi)±钏牡脑L問率的時候取所有算法中,李四訪問最低頻率的次數(shù)。所以他的名字叫Count-Min Sketch。
這里和以前的做個對比,簡單的舉個例子:如果一個hashMap來記錄這個頻率,如果我有100個數(shù)據(jù),那這個HashMap就得存儲100個這個數(shù)據(jù)的訪問頻率。哪怕我這個緩存的容量是1,因?yàn)長fu的規(guī)則我必須全部記錄這個100個數(shù)據(jù)的訪問頻率。如果有更多的數(shù)據(jù)我就有記錄更多的。
在Count-Min Sketch中,我這里直接說caffeine中的實(shí)現(xiàn)吧(在FrequencySketch這個類中),如果你的緩存大小是100,他會生成一個long數(shù)組大小是和100最接近的2的冪的數(shù),也就是128。而這個數(shù)組將會記錄我們的訪問頻率。在caffeine中他規(guī)則頻率最大為15,15的二進(jìn)制位1111,總共是4位,而Long型是64位。所以每個Long型可以放16種算法,但是caffeine并沒有這么做,只用了四種hash算法,每個Long型被分為四段,每段里面保存的是四個算法的頻率。這樣做的好處是可以進(jìn)一步減少Hash沖突,原先128大小的hash,就變成了128X4。
一個Long的結(jié)構(gòu)如下:
我們的4個段分為A,B,C,D,在后面我也會這么叫它們。而每個段里面的四個算法我叫他s1,s2,s3,s4。下面舉個例子如果要添加一個訪問50的數(shù)字頻率應(yīng)該怎么做?我們這里用size=100來舉例。
首先確定50這個hash是在哪個段里面,通過hash & 3必定能獲得小于4的數(shù)字,假設(shè)hash & 3=0,那就在A段。
對50的hash再用其他hash算法再做一次hash,得到long數(shù)組的位置。假設(shè)用s1算法得到1,s2算法得到3,s3算法得到4,s4算法得到0。
然后在long[1]的A段里面的s1位置進(jìn)行+1,簡稱1As1加1,然后在3As2加1,在4As3加1,在0As4加1。
這個時候有人會質(zhì)疑頻率最大為15的這個是否太小?沒關(guān)系在這個算法中,比如size等于100,如果他全局提升了1000次就會全局除以2衰減,衰減之后也可以繼續(xù)增加,這個算法再W-TinyLFU的論文中證明了其可以較好的適應(yīng)時間段的訪問頻率。
讀寫性能在guava cache中我們說過其讀寫操作中夾雜著過期時間的處理,也就是你在一次Put操作中有可能還會做淘汰操作,所以其讀寫性能會受到一定影響,可以看上面的圖中,caffeine的確在讀寫操作上面完爆guava cache。主要是因?yàn)樵赾affeine,對這些事件的操作是通過異步操作,他將事件提交至隊列,這里的隊列的數(shù)據(jù)結(jié)構(gòu)是RingBuffer,不清楚的可以看看這篇文章,你應(yīng)該知道的高性能無鎖隊列Disruptor。然后通過會通過默認(rèn)的ForkJoinPool.commonPool(),或者自己配置線程池,進(jìn)行取隊列操作,然后在進(jìn)行后續(xù)的淘汰,過期操作。
當(dāng)然讀寫也是有不同的隊列,在caffeine中認(rèn)為緩存讀比寫多很多,所以對于寫操作是所有線程共享一個Ringbuffer。
對于讀操作比寫操作更加頻繁,進(jìn)一步減少競爭,其為每個線程配備了一個RingBuffer:
數(shù)據(jù)淘汰策略在caffeine所有的數(shù)據(jù)都在ConcurrentHashMap中,這個和guava cache不同,guava cache是自己實(shí)現(xiàn)了個類似ConcurrentHashMap的結(jié)構(gòu)。在caffeine中有三個記錄引用的LRU隊列:
Eden隊列:在caffeine中規(guī)定只能為緩存容量的%1,如果size=100,那這個隊列的有效大小就等于1。這個隊列中記錄的是新到的數(shù)據(jù),防止突發(fā)流量由于之前沒有訪問頻率,而導(dǎo)致被淘汰。比如有一部新劇上線,在最開始其實(shí)是沒有訪問頻率的,防止上線之后被其他緩存淘汰出去,而加入這個區(qū)域。伊甸區(qū),最舒服最安逸的區(qū)域,在這里很難被其他數(shù)據(jù)淘汰。
Probation隊列:叫做緩刑隊列,在這個隊列就代表你的數(shù)據(jù)相對比較冷,馬上就要被淘汰了。這個有效大小為size減去eden減去protected。
Protected隊列:在這個隊列中,可以稍微放心一下了,你暫時不會被淘汰,但是別急,如果Probation隊列沒有數(shù)據(jù)了或者Protected數(shù)據(jù)滿了,你也將會被面臨淘汰的尷尬局面。當(dāng)然想要變成這個隊列,需要把Probation訪問一次之后,就會提升為Protected隊列。這個有效大小為(size減去eden) X 80% 如果size =100,就會是79。
這三個隊列關(guān)系如下:
所有的新數(shù)據(jù)都會進(jìn)入Eden。
Eden滿了,淘汰進(jìn)入Probation。
如果在Probation中訪問了其中某個數(shù)據(jù),則這個數(shù)據(jù)升級為Protected。
如果Protected滿了又會繼續(xù)降級為Probation。
對于發(fā)生數(shù)據(jù)淘汰的時候,會從Probation中進(jìn)行淘汰,會把這個隊列中的數(shù)據(jù)隊頭稱為受害者,這個隊頭肯定是最早進(jìn)入的,按照LRU隊列的算法的話那他其實(shí)他就應(yīng)該被淘汰,但是在這里只能叫他受害者,這個隊列是緩刑隊列,代表馬上要給他行刑了。這里會取出隊尾叫候選者,也叫攻擊者。這里受害者會和攻擊者做PK,通過我們的Count-Min Sketch中的記錄的頻率數(shù)據(jù)有以下幾個判斷:
如果攻擊者大于受害者,那么受害者就直接被淘汰。
如果攻擊者<=5,那么直接淘汰攻擊者。這個邏輯在他的注釋中有解釋:
他認(rèn)為設(shè)置一個預(yù)熱的門檻會讓整體命中率更高。
其他情況,隨機(jī)淘汰。
如何使用對于熟悉Guava的玩家來說如果擔(dān)心有切換成本,那么你完全就多慮了,caffeine的api借鑒了Guava的api,可以發(fā)現(xiàn)其基本一模一樣。
public static void main(String[] args) { Cachecache = Caffeine.newBuilder() .expireAfterWrite(1, TimeUnit.SECONDS) .expireAfterAccess(1,TimeUnit.SECONDS) .maximumSize(10) .build(); cache.put("hello","hello"); }
順便一提的是,越來越多的開源框架都放棄了Guava cache,比如Spring5。在業(yè)務(wù)上我也自己曾經(jīng)比較過Guava cache和caffeine最終選擇了caffeine,在線上也有不錯的效果。所以不用擔(dān)心caffeine不成熟,沒人使用。
最后本文主要講了愛奇藝的緩存之路和本地緩存的一個發(fā)展歷史(從古至今到未來),以及每一種緩存的實(shí)現(xiàn)基本原理。當(dāng)然要使用好緩存光是這些僅僅不夠,比如本地緩存如何在其他地方更改了之后同步更新,分布式緩存,多級緩存等等。后面也會專門寫一節(jié)介紹這個如何用好緩存。對于Guava cache和caffeine的原理后面也會專門抽出時間寫這兩個的源碼分析,如果感興趣的朋友可以關(guān)注公眾號第一時間查閱更新文章。
最后這篇文章被我收錄于JGrowing,一個全面,優(yōu)秀,由社區(qū)一起共建的Java學(xué)習(xí)路線,如果您想?yún)⑴c開源項(xiàng)目的維護(hù),可以一起共建,github地址為:https://github.com/javagrowin...
麻煩給個小星星喲。
如果你覺得這篇文章對你有文章,可以關(guān)注我的技術(shù)公眾號,你的關(guān)注和轉(zhuǎn)發(fā)是對我最大的支持,O(∩_∩)O
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/62075.html
摘要:先簡單介紹一下愛奇藝的緩存道路的發(fā)展吧。可以看見圖中分為幾個階段第一階段數(shù)據(jù)同步加通過消息隊列進(jìn)行數(shù)據(jù)同步至,然后應(yīng)用直接去取緩存這個階段優(yōu)點(diǎn)是由于是使用的分布式緩存,所以數(shù)據(jù)更新快。愛奇藝的緩存的發(fā)展也是基于此之上,通過對的二次開發(fā) 1.背景 本文是上周去技術(shù)沙龍聽了一下愛奇藝的Java緩存之路有感寫出來的。先簡單介紹一下愛奇藝的java緩存道路的發(fā)展吧。 showImg(https...
摘要:有一次別人的云服務(wù)器被攻擊,提供商竟然重啟了物理機(jī)然后又諸多悲劇出現(xiàn)。造成微博服務(wù)短暫不可用。通過建立工具來診斷問題,并創(chuàng)建一種復(fù)盤事故的文化來推動并作出改進(jìn),防止未來發(fā)生故障。 showImg(https://segmentfault.com/img/bV0jif?w=900&h=385); 相信小伙伴們在上網(wǎng)或者玩游戲的時候一定都遇到過無法訪問的情況。服務(wù)器炸了的原因有各種各樣,下...
摘要:它的設(shè)計使得即使是大型團(tuán)隊也能以高度隔離的方式應(yīng)對功能變更。獲取數(shù)據(jù)數(shù)據(jù)變更性能,都是讓人頭痛的問題。通過維護(hù)組件與數(shù)據(jù)間的依賴在依賴的數(shù)據(jù)就緒前組件不會被渲染為開發(fā)者提供更加可預(yù)測的開發(fā)環(huán)境。這杜絕了隱式的數(shù)據(jù)依賴導(dǎo)致的潛在。 關(guān)于Relay與GraphQL的介紹 原文:Introducing Relay and GraphQL 視頻地址(強(qiáng)烈建議觀看):https://www.y...
摘要:全棧開發(fā)是一個學(xué)習(xí)實(shí)現(xiàn)提高的過程。解除對開發(fā)人員的限制所有的職業(yè)都在持續(xù)的進(jìn)化。哪怕是爆炸和擁擠的印度招聘市場,全棧工程師在年也非常的搶手。印度的創(chuàng)業(yè)公司已經(jīng)開發(fā)意識到全棧工程師的重要意義,全棧會越來越重要。 在不斷壯大的招聘市場上,最需要的是有非常廣泛技術(shù)棧的人。 前言 敬愛的讀者,大家好。大家經(jīng)常討論的話題是作為一個軟件工程師是一個持續(xù)學(xué)習(xí)的過程。因?yàn)楝F(xiàn)有的趨勢和技術(shù)在軟件領(lǐng)域會很...
閱讀 801·2023-04-26 00:30
閱讀 2711·2021-11-23 09:51
閱讀 1057·2021-11-02 14:38
閱讀 2608·2021-09-07 10:23
閱讀 2255·2021-08-21 14:09
閱讀 1398·2019-08-30 10:57
閱讀 1611·2019-08-29 11:20
閱讀 1161·2019-08-26 13:53