成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

你應(yīng)該知道的緩存進(jìn)化史

Tangpj / 3854人閱讀

摘要:先簡(jiǎn)單介紹一下愛奇藝的緩存道路的發(fā)展吧。可以看見圖中分為幾個(gè)階段第一階段數(shù)據(jù)同步加通過消息隊(duì)列進(jìn)行數(shù)據(jù)同步至,然后應(yīng)用直接去取緩存這個(gè)階段優(yōu)點(diǎn)是由于是使用的分布式緩存,所以數(shù)據(jù)更新快。愛奇藝的緩存的發(fā)展也是基于此之上,通過對(duì)的二次開發(fā)

1.背景

本文是上周去技術(shù)沙龍聽了一下愛奇藝的Java緩存之路有感寫出來的。先簡(jiǎn)單介紹一下愛奇藝的java緩存道路的發(fā)展吧。


可以看見圖中分為幾個(gè)階段:

第一階段:數(shù)據(jù)同步加redis

通過消息隊(duì)列進(jìn)行數(shù)據(jù)同步至redis,然后Java應(yīng)用直接去取緩存
這個(gè)階段優(yōu)點(diǎn)是:由于是使用的分布式緩存,所以數(shù)據(jù)更新快。缺點(diǎn)也比較明顯:依賴Redis的穩(wěn)定性,一旦redis掛了,整個(gè)緩存系統(tǒng)不可用,造成緩存雪崩,所有請(qǐng)求打到DB。

第二,三階段:JavaMap到Guava cache

這個(gè)階段使用進(jìn)程內(nèi)緩存作為一級(jí)緩存,redis作為二級(jí)。優(yōu)點(diǎn):不受外部系統(tǒng)影響,其他系統(tǒng)掛了,依然能使用。缺點(diǎn):進(jìn)程內(nèi)緩存無法像分布式緩存那樣做到實(shí)時(shí)更新。由于java內(nèi)存有限,必定緩存得設(shè)置大小,然后有些緩存會(huì)被淘汰,就會(huì)有命中率的問題。

第四階段: Guava Cache刷新

為了解決上面的問題,利用Guava Cache可以設(shè)置寫后刷新時(shí)間,進(jìn)行刷新。解決了一直不更新的問題,但是依然沒有解決實(shí)時(shí)刷新。

第五階段: 外部緩存異步刷新

這個(gè)階段擴(kuò)展了Guava Cache,利用redis作為消息隊(duì)列通知機(jī)制,通知其他java應(yīng)用程序進(jìn)行刷新。

這里簡(jiǎn)單介紹一下愛奇藝緩存發(fā)展的五個(gè)階段,當(dāng)然還有一些其他的優(yōu)化,比如GC調(diào)優(yōu),緩存穿透,緩存覆蓋的一些優(yōu)化等等。有興趣的同學(xué)可以關(guān)注公眾號(hào),聯(lián)系我進(jìn)行交流。

原始社會(huì) - 查庫(kù)

上面說的是愛奇藝的一個(gè)進(jìn)化線路,但是在大家的一般開發(fā)過程中,第一步一般都沒有redis,而是直接查庫(kù)。

在流量不大的時(shí)候,查數(shù)據(jù)庫(kù)或者讀取文件是最為方便,也能完全滿足我們的業(yè)務(wù)要求。

古代社會(huì) - HashMap

當(dāng)我們應(yīng)用有一定流量之后或者查詢數(shù)據(jù)庫(kù)特別頻繁,這個(gè)時(shí)候就可以祭出我們的java中自帶的HashMap或者ConcurrentHashMap。我們可以在代碼中這么寫:

public class CustomerService {
    private HashMap 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;
    }
}

但是這樣做就有個(gè)問題HashMap無法進(jìn)行數(shù)據(jù)淘汰,內(nèi)存會(huì)無限制的增長(zhǎng),所以hashMap很快也被淘汰了。當(dāng)然并不是說他完全就沒用,就像我們古代社會(huì)也不是所有的東西都是過時(shí)的,比如我們中華名族的傳統(tǒng)美德是永不過時(shí)的,就像這個(gè)hashMap一樣的可以在某些場(chǎng)景下作為緩存,當(dāng)不需要淘汰機(jī)制的時(shí)候,比如我們利用反射,如果我們每次都通過反射去搜索Method,field,性能必定低效,這時(shí)我們用HashMap將其緩存起來,性能能提升很多。

近代社會(huì) - LRUHashMap

在古代社會(huì)中難住我們的問題無法進(jìn)行數(shù)據(jù)淘汰,這樣會(huì)導(dǎo)致我們內(nèi)存無限膨脹,顯然我們是不可以接受的。有人就說我把一些數(shù)據(jù)給淘汰掉唄,這樣不就對(duì)了,但是怎么淘汰呢?隨機(jī)淘汰嗎?當(dāng)然不行,試想一下你剛把A裝載進(jìn)緩存,下一次要訪問的時(shí)候就被淘汰了,那又會(huì)訪問我們的數(shù)據(jù)庫(kù)了,那我們要緩存干嘛呢?

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

FIFO:先進(jìn)先出,在這種淘汰算法中,先進(jìn)入緩存的會(huì)先被淘汰。這種可謂是最簡(jiǎn)單的了,但是會(huì)導(dǎo)致我們命中率很低。試想一下我們?nèi)绻袀€(gè)訪問頻率很高的數(shù)據(jù)是所有數(shù)據(jù)第一個(gè)訪問的,而那些不是很高的是后面再訪問的,那這樣就會(huì)把我們的首個(gè)數(shù)據(jù)但是他的訪問頻率很高給擠出。

LRU:最近最少使用算法。在這種算法中避免了上面的問題,每次訪問數(shù)據(jù)都會(huì)將其放在我們的隊(duì)尾,如果需要淘汰數(shù)據(jù),就只需要淘汰隊(duì)首即可。但是這個(gè)依然有個(gè)問題,如果有個(gè)數(shù)據(jù)在1個(gè)小時(shí)的前59分鐘訪問了1萬次(可見這是個(gè)熱點(diǎn)數(shù)據(jù)),再后一分鐘沒有訪問這個(gè)數(shù)據(jù),但是有其他的數(shù)據(jù)訪問,就導(dǎo)致了我們這個(gè)熱點(diǎn)數(shù)據(jù)被淘汰。

LFU:最近最少頻率使用。在這種算法中又對(duì)上面進(jìn)行了優(yōu)化,利用額外的空間記錄每個(gè)數(shù)據(jù)的使用頻率,然后選出頻率最低進(jìn)行淘汰。這樣就避免了LRU不能處理時(shí)間段的問題。

上面列舉了三種淘汰策略,對(duì)于這三種,實(shí)現(xiàn)成本是一個(gè)比一個(gè)高,同樣的命中率也是一個(gè)比一個(gè)好。而我們一般來說選擇的方案居中即可,即實(shí)現(xiàn)成本不是太高,而命中率也還行的LRU,如何實(shí)現(xiàn)一個(gè)LRUMap呢?我們可以通過繼承LinkedHashMap,重寫removeEldestEntry方法,即可完成一個(gè)簡(jiǎn)單的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的時(shí)候判斷,如果為true,就會(huì)刪除最老的
         * @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ù)了一個(gè)entry(用來放key和value的對(duì)象)鏈表。在每一次get或者put的時(shí)候都會(huì)把插入的新entry,或查詢到的老entry放在我們鏈表末尾。
可以注意到我們?cè)跇?gòu)造方法中,設(shè)置的大小特意設(shè)置到max*1.4,在下面的removeEldestEntry方法中只需要size>max就淘汰,這樣我們這個(gè)map永遠(yuǎn)也走不到擴(kuò)容的邏輯了,通過重寫LinkedHashMap,幾個(gè)簡(jiǎn)單的方法我們實(shí)現(xiàn)了我們的LruMap。

現(xiàn)代社會(huì) - Guava cache

在近代社會(huì)中已經(jīng)發(fā)明出來了LRUMap,用來進(jìn)行緩存數(shù)據(jù)的淘汰,但是有幾個(gè)問題:

鎖競(jìng)爭(zhēng)嚴(yán)重,可以看見我的代碼中,Lock是全局鎖,在方法級(jí)別上面的,當(dāng)調(diào)用量較大時(shí),性能必然會(huì)比較低。

不支持過期時(shí)間

不支持自動(dòng)刷新

所以谷歌的大佬們對(duì)于這些問題,按捺不住了,發(fā)明了Guava cache,在Guava cache中你可以如下面的代碼一樣,輕松使用:

public static void main(String[] args) throws ExecutionException {
        LoadingCache cache = CacheBuilder.newBuilder()
                .maximumSize(100)
                //寫之后30ms過期
                .expireAfterWrite(30L, TimeUnit.MILLISECONDS)
                //訪問之后30ms過期
                .expireAfterAccess(30L, TimeUnit.MILLISECONDS)
                //20ms之后刷新
                .refreshAfterWrite(20L, TimeUnit.MILLISECONDS)
                //開啟weakKey key 當(dāng)啟動(dòng)垃圾回收時(shí),該緩存也被回收
                .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;
            }
        };
    }

我將會(huì)從guava cache原理中,解釋guava cache是如何解決LRUMap的幾個(gè)問題的。

鎖競(jìng)爭(zhēng)

guava cache采用了類似ConcurrentHashMap的思想,分段加鎖,在每個(gè)段里面各自負(fù)責(zé)自己的淘汰的事情。在Guava根據(jù)一定的算法進(jìn)行分段,這里要說明的是,如果段太少那競(jìng)爭(zhēng)依然很嚴(yán)重,如果段太多會(huì)容易出現(xiàn)隨機(jī)淘汰,比如大小為100的,給他分100個(gè)段,那也就是讓每個(gè)數(shù)據(jù)都獨(dú)占一個(gè)段,而每個(gè)段會(huì)自己處理淘汰的過程,所以會(huì)出現(xiàn)隨機(jī)淘汰。在guava cache中通過如下代碼,計(jì)算出應(yīng)該如何分段。

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

上面segmentCount就是我們最后的分段數(shù),其保證了每個(gè)段至少10個(gè)Entry。如果沒有設(shè)置concurrencyLevel這個(gè)參數(shù),那么默認(rèn)就會(huì)是4,最后分段數(shù)也最多為4,例如我們size為100,會(huì)分為4段,每段最大的size是25。
在guava cache中對(duì)于寫操作直接加鎖,對(duì)于讀操作,如果讀取的數(shù)據(jù)沒有過期,且已經(jīng)加載就緒,不需要進(jìn)行加鎖,如果沒有讀到會(huì)再次加鎖進(jìn)行二次讀,如果還沒有需要進(jìn)行緩存加載,也就是通過我們配置的CacheLoader,我這里配置的是直接返回Key,在業(yè)務(wù)中通常配置從數(shù)據(jù)庫(kù)中查詢。
如下圖所示:

過期時(shí)間

相比于LRUMap多了兩種過期時(shí)間,一個(gè)是寫后多久過期expireAfterWrite,一個(gè)是讀后多久過期expireAfterAccess。很有意思的事情是,在guava cache中對(duì)于過期的Entry并沒有馬上過期(也就是并沒有后臺(tái)線程一直在掃),而是通過進(jìn)行讀寫操作的時(shí)候進(jìn)行過期處理,這樣做的好處是避免后臺(tái)線程掃描的時(shí)候進(jìn)行全局加鎖??聪旅娴拇a:

public static void main(String[] args) throws ExecutionException, InterruptedException {
        Cache 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

從這個(gè)結(jié)果中我們知道,在put的時(shí)候才進(jìn)行的過期處理。特別注意的是我上面concurrencyLevel(1)我這里將分段最大設(shè)置為1,不然不會(huì)出現(xiàn)這個(gè)實(shí)驗(yàn)效果的,在上面一節(jié)中已經(jīng)說過,我們是以段位單位進(jìn)行過期處理。在每個(gè)Segment中維護(hù)了兩個(gè)隊(duì)列:

    final Queue> writeQueue;

  
    final Queue> accessQueue;

writeQueue維護(hù)了寫隊(duì)列,隊(duì)頭代表著寫得早的數(shù)據(jù),隊(duì)尾代表寫得晚的數(shù)據(jù)。
accessQueue維護(hù)了訪問隊(duì)列,和LRU一樣,用來我們進(jìn)行訪問時(shí)間的淘汰,如果當(dāng)這個(gè)Segment超過最大容量,比如我們上面所說的25,超過之后,就會(huì)把a(bǔ)ccessQueue這個(gè)隊(duì)列的第一個(gè)元素進(jìn)行淘汰。

void expireEntries(long now) {
      drainRecencyQueue();

      ReferenceEntry 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的過程,會(huì)對(duì)兩個(gè)隊(duì)列一次進(jìn)行peek操作,如果過期就進(jìn)行刪除。一般處理過期Entries可以在我們的put操作的前后,或者讀取數(shù)據(jù)時(shí)發(fā)現(xiàn)過期了,然后進(jìn)行整個(gè)Segment的過期處理,又或者進(jìn)行二次讀lockedGetOrLoad操作的時(shí)候調(diào)用。

void evictEntries(ReferenceEntry newest) {
      ///... 省略無用代碼

      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的時(shí)候的代碼,可以看見訪問的是accessQueue對(duì)其隊(duì)頭進(jìn)行驅(qū)逐。而驅(qū)逐策略一般是在對(duì)segment中的元素發(fā)生變化時(shí)進(jìn)行調(diào)用,比如插入操作,更新操作,加載數(shù)據(jù)操作。

自動(dòng)刷新

自動(dòng)刷新操作,在guava cache中實(shí)現(xiàn)相對(duì)比較簡(jiǎn)單,直接通過查詢,判斷其是否滿足刷新條件,進(jìn)行刷新。

其他特性

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

虛引用

在Guava cache中,key和value都能進(jìn)行虛引用的設(shè)定,在Segment中的有兩個(gè)引用隊(duì)列:

    final @Nullable ReferenceQueue keyReferenceQueue;

  
    final @Nullable ReferenceQueue valueReferenceQueue;

這兩個(gè)隊(duì)列用來記錄被回收的引用,其中每個(gè)隊(duì)列記錄了每個(gè)被回收的Entry的hash,這樣回收了之后通過這個(gè)隊(duì)列中的hash值就能把以前的Entry進(jìn)行刪除。

刪除監(jiān)聽器

在guava cache中,當(dāng)有數(shù)據(jù)被淘汰時(shí),但是你不知道他到底是過期,還是被驅(qū)逐,還是因?yàn)樘撘玫膶?duì)象被回收?這個(gè)時(shí)候你可以調(diào)用這個(gè)方法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í)就是一個(gè)性能不錯(cuò)的,api豐富的LRU Map。愛奇藝的緩存的發(fā)展也是基于此之上,通過對(duì)guava cache的二次開發(fā),讓其可以進(jìn)行java應(yīng)用服務(wù)之間的緩存更新。

走向未來-caffeine

guava cache的功能的確是很強(qiáng)大,滿足了絕大多數(shù)的人的需求,但是其本質(zhì)上還是LRU的一層封裝,所以在眾多其他較為優(yōu)良的淘汰算法中就相形見絀了。而caffeine cache實(shí)現(xiàn)了W-TinyLFU(LFU+LRU算法的變種)。下面是不同算法的命中率的比較:

其中Optimal是最理想的命中率,LRU和其他算法相比的確是個(gè)弟弟。而我們的W-TinyLFU 是最接近理想命中率的。當(dāng)然不僅僅是命中率caffeine優(yōu)于了guava cache,在讀寫吞吐量上面也是完爆guava cache。

這個(gè)時(shí)候你肯定會(huì)好奇為啥這么caffeine這么牛逼呢?別著急下面慢慢給你道來。

W-TinyLFU

上面已經(jīng)說過了傳統(tǒng)的LFU是怎么一回事。在LFU中只要數(shù)據(jù)訪問模式的概率分布隨時(shí)間保持不變時(shí),其命中率就能變得非常高。這里我還是拿愛奇藝舉例,比如有部新劇出來了,我們使用LFU給他緩存下來,這部新劇在這幾天大概訪問了幾億次,這個(gè)訪問頻率也在我們的LFU中記錄了幾億次。但是新劇總會(huì)過氣的,比如一個(gè)月之后這個(gè)新劇的前幾集其實(shí)已經(jīng)過氣了,但是他的訪問量的確是太高了,其他的電視劇根本無法淘汰這個(gè)新劇,所以在這種模式下是有局限性。所以各種LFU的變種出現(xiàn)了,基于時(shí)間周期進(jìn)行衰減,或者在最近某個(gè)時(shí)間段內(nèi)的頻率。同樣的LFU也會(huì)使用額外空間記錄每一個(gè)數(shù)據(jù)訪問的頻率,即使數(shù)據(jù)沒有在緩存中也需要記錄,所以需要維護(hù)的額外空間很大。

可以試想我們對(duì)這個(gè)維護(hù)空間建立一個(gè)hashMap,每個(gè)數(shù)據(jù)項(xiàng)都會(huì)存在這個(gè)hashMap中,當(dāng)數(shù)據(jù)量特別大的時(shí)候,這個(gè)hashMap也會(huì)特別大。

再回到LRU,我們的LRU也不是那么一無是處,LRU可以很好的應(yīng)對(duì)突發(fā)流量的情況,因?yàn)樗恍枰塾?jì)數(shù)據(jù)頻率。

所以W-TinyLFU結(jié)合了LRU和LFU,以及其他的算法的一些特點(diǎn)。

頻率記錄

首先要說到的就是頻率記錄的問題,我們要實(shí)現(xiàn)的目標(biāo)是利用有限的空間可以記錄隨時(shí)間變化的訪問頻率。在W-TinyLFU中使用Count-Min Sketch記錄我們的訪問頻率,而這個(gè)也是布隆過濾器的一種變種。如下圖所示:

如果需要記錄一個(gè)值,那我們需要通過多種Hash算法對(duì)其進(jìn)行處理hash,然后在對(duì)應(yīng)的hash算法的記錄中+1,為什么需要多種hash算法呢?由于這是一個(gè)壓縮算法必定會(huì)出現(xiàn)沖突,比如我們建立一個(gè)Long的數(shù)組,通過計(jì)算出每個(gè)數(shù)據(jù)的hash的位置。比如張三和李四,他們兩有可能hash值都是相同,比如都是1那Long[1]這個(gè)位置就會(huì)增加相應(yīng)的頻率,張三訪問1萬次,李四訪問1次那Long[1]這個(gè)位置就是1萬零1,如果取李四的訪問評(píng)率的時(shí)候就會(huì)取出是1萬零1,但是李四命名只訪問了1次啊,為了解決這個(gè)問題,所以用了多個(gè)hash算法可以理解為long[][]二維數(shù)組的一個(gè)概念,比如在第一個(gè)算法張三和李四沖突了,但是在第二個(gè),第三個(gè)中很大的概率不沖突,比如一個(gè)算法大概有1%的概率沖突,那四個(gè)算法一起沖突的概率是1%的四次方。通過這個(gè)模式我們?nèi)±钏牡脑L問率的時(shí)候取所有算法中,李四訪問最低頻率的次數(shù)。所以他的名字叫Count-Min Sketch。

這里和以前的做個(gè)對(duì)比,簡(jiǎn)單的舉個(gè)例子:如果一個(gè)hashMap來記錄這個(gè)頻率,如果我有100個(gè)數(shù)據(jù),那這個(gè)HashMap就得存儲(chǔ)100個(gè)這個(gè)數(shù)據(jù)的訪問頻率。哪怕我這個(gè)緩存的容量是1,因?yàn)長(zhǎng)fu的規(guī)則我必須全部記錄這個(gè)100個(gè)數(shù)據(jù)的訪問頻率。如果有更多的數(shù)據(jù)我就有記錄更多的。

在Count-Min Sketch中,我這里直接說caffeine中的實(shí)現(xiàn)吧(在FrequencySketch這個(gè)類中),如果你的緩存大小是100,他會(huì)生成一個(gè)long數(shù)組大小是和100最接近的2的冪的數(shù),也就是128。而這個(gè)數(shù)組將會(huì)記錄我們的訪問頻率。在caffeine中他規(guī)則頻率最大為15,15的二進(jìn)制位1111,總共是4位,而Long型是64位。所以每個(gè)Long型可以放16種算法,但是caffeine并沒有這么做,只用了四種hash算法,每個(gè)Long型被分為四段,每段里面保存的是四個(gè)算法的頻率。這樣做的好處是可以進(jìn)一步減少Hash沖突,原先128大小的hash,就變成了128X4。

一個(gè)Long的結(jié)構(gòu)如下:

我們的4個(gè)段分為A,B,C,D,在后面我也會(huì)這么叫它們。而每個(gè)段里面的四個(gè)算法我叫他s1,s2,s3,s4。下面舉個(gè)例子如果要添加一個(gè)訪問50的數(shù)字頻率應(yīng)該怎么做?我們這里用size=100來舉例。

首先確定50這個(gè)hash是在哪個(gè)段里面,通過hash & 3必定能獲得小于4的數(shù)字,假設(shè)hash & 3=0,那就在A段。

對(duì)50的hash再用其他hash算法再做一次hash,得到long數(shù)組的位置。假設(shè)用s1算法得到1,s2算法得到3,s3算法得到4,s4算法得到0。

然后在long[1]的A段里面的s1位置進(jìn)行+1,簡(jiǎn)稱1As1加1,然后在3As2加1,在4As3加1,在0As4加1。

這個(gè)時(shí)候有人會(huì)質(zhì)疑頻率最大為15的這個(gè)是否太?。繘]關(guān)系在這個(gè)算法中,比如size等于100,如果他全局提升了1000次就會(huì)全局除以2衰減,衰減之后也可以繼續(xù)增加,這個(gè)算法再W-TinyLFU的論文中證明了其可以較好的適應(yīng)時(shí)間段的訪問頻率。

讀寫性能

在guava cache中我們說過其讀寫操作中夾雜著過期時(shí)間的處理,也就是你在一次Put操作中有可能還會(huì)做淘汰操作,所以其讀寫性能會(huì)受到一定影響,可以看上面的圖中,caffeine的確在讀寫操作上面完爆guava cache。主要是因?yàn)樵赾affeine,對(duì)這些事件的操作是通過異步操作,他將事件提交至隊(duì)列,這里的隊(duì)列的數(shù)據(jù)結(jié)構(gòu)是RingBuffer,不清楚的可以看看這篇文章,你應(yīng)該知道的高性能無鎖隊(duì)列Disruptor。然后通過會(huì)通過默認(rèn)的ForkJoinPool.commonPool(),或者自己配置線程池,進(jìn)行取隊(duì)列操作,然后在進(jìn)行后續(xù)的淘汰,過期操作。

當(dāng)然讀寫也是有不同的隊(duì)列,在caffeine中認(rèn)為緩存讀比寫多很多,所以對(duì)于寫操作是所有線程共享一個(gè)Ringbuffer。

對(duì)于讀操作比寫操作更加頻繁,進(jìn)一步減少競(jìng)爭(zhēng),其為每個(gè)線程配備了一個(gè)RingBuffer:

數(shù)據(jù)淘汰策略

在caffeine所有的數(shù)據(jù)都在ConcurrentHashMap中,這個(gè)和guava cache不同,guava cache是自己實(shí)現(xiàn)了個(gè)類似ConcurrentHashMap的結(jié)構(gòu)。在caffeine中有三個(gè)記錄引用的LRU隊(duì)列:

Eden隊(duì)列:在caffeine中規(guī)定只能為緩存容量的%1,如果size=100,那這個(gè)隊(duì)列的有效大小就等于1。這個(gè)隊(duì)列中記錄的是新到的數(shù)據(jù),防止突發(fā)流量由于之前沒有訪問頻率,而導(dǎo)致被淘汰。比如有一部新劇上線,在最開始其實(shí)是沒有訪問頻率的,防止上線之后被其他緩存淘汰出去,而加入這個(gè)區(qū)域。伊甸區(qū),最舒服最安逸的區(qū)域,在這里很難被其他數(shù)據(jù)淘汰。

Probation隊(duì)列:叫做緩刑隊(duì)列,在這個(gè)隊(duì)列就代表你的數(shù)據(jù)相對(duì)比較冷,馬上就要被淘汰了。這個(gè)有效大小為size減去eden減去protected。

Protected隊(duì)列:在這個(gè)隊(duì)列中,可以稍微放心一下了,你暫時(shí)不會(huì)被淘汰,但是別急,如果Probation隊(duì)列沒有數(shù)據(jù)了或者Protected數(shù)據(jù)滿了,你也將會(huì)被面臨淘汰的尷尬局面。當(dāng)然想要變成這個(gè)隊(duì)列,需要把Probation訪問一次之后,就會(huì)提升為Protected隊(duì)列。這個(gè)有效大小為(size減去eden) X 80% 如果size =100,就會(huì)是79。

這三個(gè)隊(duì)列關(guān)系如下:

所有的新數(shù)據(jù)都會(huì)進(jìn)入Eden。

Eden滿了,淘汰進(jìn)入Probation。

如果在Probation中訪問了其中某個(gè)數(shù)據(jù),則這個(gè)數(shù)據(jù)升級(jí)為Protected。

如果Protected滿了又會(huì)繼續(xù)降級(jí)為Probation。

對(duì)于發(fā)生數(shù)據(jù)淘汰的時(shí)候,會(huì)從Probation中進(jìn)行淘汰,會(huì)把這個(gè)隊(duì)列中的數(shù)據(jù)隊(duì)頭稱為受害者,這個(gè)隊(duì)頭肯定是最早進(jìn)入的,按照LRU隊(duì)列的算法的話那他其實(shí)他就應(yīng)該被淘汰,但是在這里只能叫他受害者,這個(gè)隊(duì)列是緩刑隊(duì)列,代表馬上要給他行刑了。這里會(huì)取出隊(duì)尾叫候選者,也叫攻擊者。這里受害者會(huì)和攻擊者做PK,通過我們的Count-Min Sketch中的記錄的頻率數(shù)據(jù)有以下幾個(gè)判斷:

如果攻擊者大于受害者,那么受害者就直接被淘汰。

如果攻擊者<=5,那么直接淘汰攻擊者。這個(gè)邏輯在他的注釋中有解釋:


他認(rèn)為設(shè)置一個(gè)預(yù)熱的門檻會(huì)讓整體命中率更高。

其他情況,隨機(jī)淘汰。

如何使用

對(duì)于熟悉Guava的玩家來說如果擔(dān)心有切換成本,那么你完全就多慮了,caffeine的api借鑒了Guava的api,可以發(fā)現(xiàn)其基本一模一樣。

public static void main(String[] args) {
        Cache cache = 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,在線上也有不錯(cuò)的效果。所以不用擔(dān)心caffeine不成熟,沒人使用。

最后

本文主要講了愛奇藝的緩存之路和本地緩存的一個(gè)發(fā)展歷史(從古至今到未來),以及每一種緩存的實(shí)現(xiàn)基本原理。當(dāng)然要使用好緩存光是這些僅僅不夠,比如本地緩存如何在其他地方更改了之后同步更新,分布式緩存,多級(jí)緩存等等。后面也會(huì)專門寫一節(jié)介紹這個(gè)如何用好緩存。對(duì)于Guava cache和caffeine的原理后面也會(huì)專門抽出時(shí)間寫這兩個(gè)的源碼分析,如果感興趣的朋友可以關(guān)注公眾號(hào)第一時(shí)間查閱更新文章。

最后這篇文章被我收錄于JGrowing,一個(gè)全面,優(yōu)秀,由社區(qū)一起共建的Java學(xué)習(xí)路線,如果您想?yún)⑴c開源項(xiàng)目的維護(hù),可以一起共建,github地址為:https://github.com/javagrowin...
麻煩給個(gè)小星星喲。

如果你覺得這篇文章對(duì)你有文章,可以關(guān)注我的技術(shù)公眾號(hào),你的關(guān)注和轉(zhuǎn)發(fā)是對(duì)我最大的支持,O(∩_∩)O

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/62075.html

相關(guān)文章

  • 應(yīng)該知道緩存進(jìn)化史

    摘要:先簡(jiǎn)單介紹一下愛奇藝的緩存道路的發(fā)展吧。可以看見圖中分為幾個(gè)階段第一階段數(shù)據(jù)同步加通過消息隊(duì)列進(jìn)行數(shù)據(jù)同步至,然后應(yīng)用直接去取緩存這個(gè)階段優(yōu)點(diǎn)是由于是使用的分布式緩存,所以數(shù)據(jù)更新快。愛奇藝的緩存的發(fā)展也是基于此之上,通過對(duì)的二次開發(fā) 1.背景 本文是上周去技術(shù)沙龍聽了一下愛奇藝的Java緩存之路有感寫出來的。先簡(jiǎn)單介紹一下愛奇藝的java緩存道路的發(fā)展吧。 showImg(https...

    remcarpediem 評(píng)論0 收藏0
  • SegmentFault 技術(shù)周刊 Vol.39 - 什么!服務(wù)器炸了?

    摘要:有一次別人的云服務(wù)器被攻擊,提供商竟然重啟了物理機(jī)然后又諸多悲劇出現(xiàn)。造成微博服務(wù)短暫不可用。通過建立工具來診斷問題,并創(chuàng)建一種復(fù)盤事故的文化來推動(dòng)并作出改進(jìn),防止未來發(fā)生故障。 showImg(https://segmentfault.com/img/bV0jif?w=900&h=385); 相信小伙伴們?cè)谏暇W(wǎng)或者玩游戲的時(shí)候一定都遇到過無法訪問的情況。服務(wù)器炸了的原因有各種各樣,下...

    1treeS 評(píng)論0 收藏0
  • Flux再進(jìn)化:Introducing Relay and GraphQL譯

    摘要:它的設(shè)計(jì)使得即使是大型團(tuán)隊(duì)也能以高度隔離的方式應(yīng)對(duì)功能變更。獲取數(shù)據(jù)數(shù)據(jù)變更性能,都是讓人頭痛的問題。通過維護(hù)組件與數(shù)據(jù)間的依賴在依賴的數(shù)據(jù)就緒前組件不會(huì)被渲染為開發(fā)者提供更加可預(yù)測(cè)的開發(fā)環(huán)境。這杜絕了隱式的數(shù)據(jù)依賴導(dǎo)致的潛在。 關(guān)于Relay與GraphQL的介紹 原文:Introducing Relay and GraphQL 視頻地址(強(qiáng)烈建議觀看):https://www.y...

    cncoder 評(píng)論0 收藏0
  • 全棧開發(fā):2017年最好選擇[翻譯]

    摘要:全棧開發(fā)是一個(gè)學(xué)習(xí)實(shí)現(xiàn)提高的過程。解除對(duì)開發(fā)人員的限制所有的職業(yè)都在持續(xù)的進(jìn)化。哪怕是爆炸和擁擠的印度招聘市場(chǎng),全棧工程師在年也非常的搶手。印度的創(chuàng)業(yè)公司已經(jīng)開發(fā)意識(shí)到全棧工程師的重要意義,全棧會(huì)越來越重要。 在不斷壯大的招聘市場(chǎng)上,最需要的是有非常廣泛技術(shù)棧的人。 前言 敬愛的讀者,大家好。大家經(jīng)常討論的話題是作為一個(gè)軟件工程師是一個(gè)持續(xù)學(xué)習(xí)的過程。因?yàn)楝F(xiàn)有的趨勢(shì)和技術(shù)在軟件領(lǐng)域會(huì)很...

    fireflow 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<