W-TinyLFU--在caffeine缓存组件中的应用

一、引言

缓存-淘汰策略原理及其实现中,我们提到了常用的LRU和LFU各自的优缺点,为了综合LRU和LFU各自的优点,最后演进出了W-TinyLFU算法。该算法既能够应对突发性的流量场景,又能够应对局部周期性热点数据的场景。下面我们将详细讲解W-TinyLFU在caffeine中的设计思想与源码解析。

W-TinyLFU的总体设计思想

W-TinyLFU为了平衡访问频率和访问时间新鲜程度两个维度因素,尽量
将新鲜的访问频率高的缓存项保留在缓存中,将缓存存储空间分为两个大的区域:Window Cache和Main Cache,


image.png

Window Cache是一个标准的LRU Cache,Main Cache则是一个SLFU(Segmemted LFU)cache,Main Cache进一步划分为Protected Cache(保护区)和Probation Cache(考察区)两个区域,这两个区域都是基于LRU的Cache。


image.png

这三个区域的作用分别是:

  • window cache区域用来存储刚产生的数据;
  • Protected Cache则用来存放频率较高的热点数据;
  • 介于这两者之间的,即兼顾了访问时间和访问频率的数据则存放于Probation Cache。

W-TinyLFU的算法流程

1、数据刚进入时,如果window区域未满,则放入队尾;如果缓存个数超过了window区域的最大缓存数,则将元素放入队首。
2、缓存区域维护

  • 首先维护window区域,如果window区超过了最大的限制,那么就要把“多出来”的记录做处理。取队首元素,从window中删除,并且把节点移动到probation区域的队首。
  • 接着维护mainCache区域。会从probation队列取出队首元素victim(刚从window区域过来的)和队尾元素candidate(最久未被访问过的)pk,比较频率大小,小的元素会被从缓存中删除。
  • producted 区域维护:probation中的元素如果被访问过,元素则会晋升到producted区域。如果producted区域元素满了,则根据lru会淘汰出一个元素进入probgation(不同版本有差别)
    整体的淘汰流程如下图所示:


    image.png

总结

从上面对W-TinyLFU的原理描述可知,caffeine综合了LFU和LRU的优势,将不同特性的缓存项存入不同的缓存区域,最近刚产生的缓存项进入Window区,不会被淘汰;访问频率高的缓存项进入Protected区,也不会淘汰;介于这两者之间的缓存项存在Probation区,当缓存空间满了时,Probation区的缓存项会根据访问频率判断是保留还是淘汰;通过这种机制,很好的平衡了访问频率和访问时间新鲜程度两个维度因素,尽量将新鲜的访问频率高的缓存项保留在缓存中。
同时在维护缓存项访问频率时,引入计数器衰减机制(保鲜),即节省了存储资源,也能较好的处理稀疏流量、短时超热点流量等传统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()) {
       //当前缓存个数
        long weightedSize = weightedSize();
        //设置当前缓存个数
        setWeightedSize(weightedSize + weight);
        //设置window区域缓存个数
        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);
        }
        //如果有驱逐策略并且当前缓存个数已经大于了window区域最大的缓存数,
        //则把新来的缓存key放到deque的头部
        if (evicts() && (weight > windowMaximum())) {
          accessOrderWindowDeque().offerFirst(node);
        } else if (evicts() || expiresAfterAccess()) {
          //如果有驱逐策略(并且当前缓存个数已经小于window区域最大的缓存 
          //数) 或者 设置了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区淘汰
int evictFromWindow() {
    int candidates = 0;
    //取window deque的队首第一个元素(LRU策略,最先达到的元素在队尾,后 
    //到的元素在其前面)
    Node<K, V> node = accessOrderWindowDeque().peek();
    //一直循环: 如果window区超过了最大的限制,那么就要把“多出来”的记录做处理
    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) {
        //设置 node 的类型: 为观察类型 probation
        node.makeMainProbation();
       // 从window区去掉
        accessOrderWindowDeque().remove(node);
        //加入到probation queue,相当于把节点移动到probation区(开始准备晋升)
        accessOrderProbationDeque().add(node);
        candidates++;

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

    return candidates;
  }

  //从mainCache区淘汰
  void evictFromMain(int candidates) {
    int victimQueue = PROBATION;
    //victim 从probation cache区域取出队首元素(window区域淘汰的)
    Node<K, V> victim = accessOrderProbationDeque().peekFirst();
    //candidate = 从probation cache区域取出队尾元素(最久没有被访问过)
    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);
      }
    }
  }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容