W-TinyLFU--在caffeine緩存組件中的應(yīng)用

一、引言

緩存-淘汰策略原理及其實現(xiàn)中茬底,我們提到了常用的LRU和LFU各自的優(yōu)缺點高诺,為了綜合LRU和LFU各自的優(yōu)點,最后演進出了W-TinyLFU算法差购。該算法既能夠應(yīng)對突發(fā)性的流量場景绳锅,又能夠應(yīng)對局部周期性熱點數(shù)據(jù)的場景飒焦。下面我們將詳細講解W-TinyLFU在caffeine中的設(shè)計思想與源碼解析那槽。

W-TinyLFU的總體設(shè)計思想

W-TinyLFU為了平衡訪問頻率和訪問時間新鮮程度兩個維度因素悼沿,盡量
將新鮮的訪問頻率高的緩存項保留在緩存中,將緩存存儲空間分為兩個大的區(qū)域:Window Cache和Main Cache骚灸,


image.png

Window Cache是一個標準的LRU Cache糟趾,Main Cache則是一個SLFU(Segmemted LFU)cache,Main Cache進一步劃分為Protected Cache(保護區(qū))和Probation Cache(考察區(qū))兩個區(qū)域甚牲,這兩個區(qū)域都是基于LRU的Cache拉讯。


image.png

這三個區(qū)域的作用分別是:

  • window cache區(qū)域用來存儲剛產(chǎn)生的數(shù)據(jù);
  • Protected Cache則用來存放頻率較高的熱點數(shù)據(jù)鳖藕;
  • 介于這兩者之間的,即兼顧了訪問時間和訪問頻率的數(shù)據(jù)則存放于Probation Cache只锭。

W-TinyLFU的算法流程

1著恩、數(shù)據(jù)剛進入時,如果window區(qū)域未滿蜻展,則放入隊尾喉誊;如果緩存?zhèn)€數(shù)超過了window區(qū)域的最大緩存數(shù),則將元素放入隊首纵顾。
2伍茄、緩存區(qū)域維護

  • 首先維護window區(qū)域,如果window區(qū)超過了最大的限制施逾,那么就要把“多出來”的記錄做處理敷矫。取隊首元素,從window中刪除汉额,并且把節(jié)點移動到probation區(qū)域的隊首曹仗。
  • 接著維護mainCache區(qū)域。會從probation隊列取出隊首元素victim(剛從window區(qū)域過來的)和隊尾元素candidate(最久未被訪問過的)pk蠕搜,比較頻率大小怎茫,小的元素會被從緩存中刪除。
  • producted 區(qū)域維護:probation中的元素如果被訪問過妓灌,元素則會晉升到producted區(qū)域轨蛤。如果producted區(qū)域元素滿了蜜宪,則根據(jù)lru會淘汰出一個元素進入probgation(不同版本有差別)
    整體的淘汰流程如下圖所示:


    image.png

總結(jié)

從上面對W-TinyLFU的原理描述可知,caffeine綜合了LFU和LRU的優(yōu)勢祥山,將不同特性的緩存項存入不同的緩存區(qū)域圃验,最近剛產(chǎn)生的緩存項進入Window區(qū),不會被淘汰枪蘑;訪問頻率高的緩存項進入Protected區(qū)损谦,也不會淘汰;介于這兩者之間的緩存項存在Probation區(qū)岳颇,當緩存空間滿了時照捡,Probation區(qū)的緩存項會根據(jù)訪問頻率判斷是保留還是淘汰;通過這種機制话侧,很好的平衡了訪問頻率和訪問時間新鮮程度兩個維度因素栗精,盡量將新鮮的訪問頻率高的緩存項保留在緩存中。
同時在維護緩存項訪問頻率時瞻鹏,引入計數(shù)器衰減機制(保鮮)悲立,即節(jié)省了存儲資源,也能較好的處理稀疏流量新博、短時超熱點流量等傳統(tǒng)LFU無法很好處理的場景薪夕。

附錄:caffeine源碼

 final class AddTask implements Runnable {
    final Node<K, V> node;
    final int weight;

    AddTask(Node<K, V> node, int weight) {
      this.weight = weight;
      this.node = node;
    }

    @Override
    @GuardedBy("evictionLock")
    @SuppressWarnings("FutureReturnValueIgnored")
    public void run() {
      if (evicts()) {
       //當前緩存?zhèn)€數(shù)
        long weightedSize = weightedSize();
        //設(shè)置當前緩存?zhèn)€數(shù)
        setWeightedSize(weightedSize + weight);
        //設(shè)置window區(qū)域緩存?zhèn)€數(shù)
        setWindowWeightedSize(windowWeightedSize() + weight);
        node.setPolicyWeight(node.getPolicyWeight() + weight);

        long maximum = maximum();
        if (weightedSize >= (maximum >>> 1)) {
          // Lazily initialize when close to the maximum
          long capacity = isWeighted() ? data.mappingCount() : maximum;
          frequencySketch().ensureCapacity(capacity);
        }

        K key = node.getKey();
        if (key != null) {
          //更新訪問頻率
          frequencySketch().increment(key);
        }

        setMissesInSample(missesInSample() + 1);
      }

      // ignore out-of-order write operations
      boolean isAlive;
      synchronized (node) {
        isAlive = node.isAlive();
      }
      if (isAlive) {
        if (expiresAfterWrite()) {
          writeOrderDeque().add(node);
        }
        //如果有驅(qū)逐策略并且當前緩存?zhèn)€數(shù)已經(jīng)大于了window區(qū)域最大的緩存數(shù),
        //則把新來的緩存key放到deque的頭部
        if (evicts() && (weight > windowMaximum())) {
          accessOrderWindowDeque().offerFirst(node);
        } else if (evicts() || expiresAfterAccess()) {
          //如果有驅(qū)逐策略(并且當前緩存?zhèn)€數(shù)已經(jīng)小于window區(qū)域最大的緩存 
          //數(shù)) 或者 設(shè)置了key進入后失效赫悄,則把緩存key放到deque的尾部
          accessOrderWindowDeque().offerLast(node);
        }
        if (expiresVariable()) {
          timerWheel().schedule(node);
        }
      }

      // Ensure that in-flight async computation cannot expire (reset on a completion callback)
      if (isComputingAsync(node)) {
        synchronized (node) {
          if (!Async.isReady((CompletableFuture<?>) node.getValue())) {
            long expirationTime = expirationTicker().read() + ASYNC_EXPIRY;
            setVariableTime(node, expirationTime);
            setAccessTime(node, expirationTime);
            setWriteTime(node, expirationTime);
          }
        }
      }
    }
  }



//從window區(qū)淘汰
int evictFromWindow() {
    int candidates = 0;
    //取window deque的隊首第一個元素(LRU策略原献,最先達到的元素在隊尾,后 
    //到的元素在其前面)
    Node<K, V> node = accessOrderWindowDeque().peek();
    //一直循環(huán): 如果window區(qū)超過了最大的限制埂淮,那么就要把“多出來”的記錄做處理
    while (windowWeightedSize() > windowMaximum()) {
      // The pending operations will adjust the size to reflect the correct weight
      if (node == null) {
        break;
      }

      Node<K, V> next = node.getNextInAccessOrder();
      if (node.getPolicyWeight() != 0) {
        //設(shè)置 node 的類型: 為觀察類型 probation
        node.makeMainProbation();
       // 從window區(qū)去掉
        accessOrderWindowDeque().remove(node);
        //加入到probation queue,相當于把節(jié)點移動到probation區(qū)(開始準備晉升)
        accessOrderProbationDeque().add(node);
        candidates++;

        setWindowWeightedSize(windowWeightedSize() - node.getPolicyWeight());
      }
      node = next;
    }

    return candidates;
  }

  //從mainCache區(qū)淘汰
  void evictFromMain(int candidates) {
    int victimQueue = PROBATION;
    //victim 從probation cache區(qū)域取出隊首元素(window區(qū)域淘汰的)
    Node<K, V> victim = accessOrderProbationDeque().peekFirst();
    //candidate = 從probation cache區(qū)域取出隊尾元素(最久沒有被訪問過)
    Node<K, V> candidate = accessOrderProbationDeque().peekLast();
    while (weightedSize() > maximum()) {
      (省略部分代碼)
      ......... 

      /**
       *candidate和victim進行頻率比較姑隅,頻率小的會被從緩存中刪除。
       */
      if (admit(candidateKey, victimKey)) {
        Node<K, V> evict = victim;
        victim = victim.getNextInAccessOrder();
        // 刪除 victim 倔撞,從而留下 candidate
        evictEntry(evict, RemovalCause.SIZE, 0L);
        candidate = candidate.getPreviousInAccessOrder();
      } else {
        Node<K, V> evict = candidate;
        candidate = (candidates > 0)
            ? candidate.getPreviousInAccessOrder()
            : candidate.getNextInAccessOrder();
        // 刪除 candidate 讲仰,從而留下 victim
        evictEntry(evict, RemovalCause.SIZE, 0L);
      }
    }
  }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市痪蝇,隨后出現(xiàn)的幾起案子鄙陡,更是在濱河造成了極大的恐慌,老刑警劉巖躏啰,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件柔吼,死亡現(xiàn)場離奇詭異,居然都是意外死亡丙唧,警方通過查閱死者的電腦和手機愈魏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來培漏,“玉大人,你說我怎么就攤上這事牌柄』” “怎么了珊佣?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵蹋宦,是天一觀的道長。 經(jīng)常有香客問我咒锻,道長冷冗,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任惑艇,我火速辦了婚禮,結(jié)果婚禮上滨巴,老公的妹妹穿的比我還像新娘思灌。我一直安慰自己恭取,他們只是感情好泰偿,可當我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蜈垮,像睡著了一般甜奄。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上窃款,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天,我揣著相機與錄音牍氛,去河邊找鬼晨继。 笑死搬俊,一個胖子當著我的面吹牛紊扬,可吹牛的內(nèi)容都是我干的唉擂。 我是一名探鬼主播餐屎,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼玩祟,長吁一口氣:“原來是場噩夢啊……” “哼腹缩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起藏鹊,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎盘寡,沒想到半個月后楚殿,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡脆粥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年影涉,在試婚紗的時候發(fā)現(xiàn)自己被綠了变隔。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片常潮。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡弟胀,死狀恐怖喊式,靈堂內(nèi)的尸體忽然破棺而出孵户,到底是詐尸還是另有隱情岔留,我是刑警寧澤夏哭,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布献联,位于F島的核電站竖配,受9級特大地震影響里逆,放射性物質(zhì)發(fā)生泄漏进胯。R本人自食惡果不足惜原押,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一胁镐、第九天 我趴在偏房一處隱蔽的房頂上張望诸衔。 院中可真熱鬧盯漂,春花似錦笨农、人聲如沸就缆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至羞延,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間伴箩,已是汗流浹背入愧。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工嗤谚, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留棺蛛,地道東北人巩步。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓旁赊,卻偏偏與公主長得像椅野,于是被迫代替她去往敵國和親终畅。 傳聞我的和親對象是個殘疾皇子竟闪,可洞房花燭夜當晚...
    茶點故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內(nèi)容