caffeine 淘汰策略

Caffeine在开启了淘汰策略的时候,维护了三条队列,分别是eden,protected,probation队列。具体的三条队列大小分配方案如下所示,

long max = Math.min(maximum, MAXIMUM_CAPACITY);
long eden = max - (long) (max * PERCENT_MAIN);
long mainProtected = (long) ((max - eden) * PERCENT_MAIN_PROTECTED);

其中,eden为总大小的百分之1,当总大小为1000的时候,eden也只有10的长度,而protected则占剩下百分之99的百分之80,也就是800,剩下的百分之20则为probation队列,大小则为190。

 

当数据刚添加的时候将会将其加入到eden队列,eden队列中的数据不会具体参与到淘汰的过程中,保证在lfu算法下新进入的数据不会由于刚进入而被淘汰。

淘汰的具体方法为evictEntries()方法。

void evictEntries() {
  if (!evicts()) {
    return;
  }
  int candidates = evictFromEden();
  evictFromMain(candidates);
}

其中,一共分为两步,第一步则通过evictFromEden()方法将eden队列中的元素个数平衡到eden的最大数量以下,并将移出的元素放到probation队列中,而后通过evictFromMain()方法开始从probation和protected淘汰数据。

int evictFromEden() {
  int candidates = 0;
  Node<K, V> node = accessOrderEdenDeque().peek();
  while (edenWeightedSize() > edenMaximum()) {
    // The pending operations will adjust the size to reflect the correct weight
    if (node == null) {
      break;
    }

    Node<K, V> next = node.getNextInAccessOrder();
    if (node.getWeight() != 0) {
      node.makeMainProbation();
      accessOrderEdenDeque().remove(node);
      accessOrderProbationDeque().add(node);
      candidates++;

      lazySetEdenWeightedSize(edenWeightedSize() - node.getPolicyWeight());
    }
    node = next;
  }

  return candidates;
}

evictFromEden()方法的实现如上,逻辑很简单,当eden元素个数大于总量的百分之一的时候,从eden队首获取元素,并将其移到probation的队尾重复该操作,直到eden剩余数量小于预设的eden长度为止。该方法会返回从eden移入到probation的个数,这几个新加入probation的成员会优先与probation的队首键进行频度比较。

而后,通过evictFromMain()方法开始正式进行淘汰,其中,优先会从probation中将数据进行淘汰,直到probation队列为空才会从protected队列中获取元素尝试淘汰,而当protected也而无法获取元素,则会从protected队列中进行获取。probation中的元素被外部访问一次就会被晋升到protected队列。

int victimQueue = PROBATION;
Node<K, V> victim = accessOrderProbationDeque().peekFirst();
Node<K, V> candidate = accessOrderProbationDeque().peekLast();

evictFromMain()方法很长,首先,会从probation中获取队首和队尾两个元素,其中,队尾的元素为刚刚上一步从eden移入到probation的元素。

if ((candidate == null) && (victim == null)) {
  if (victimQueue == PROBATION) {
    victim = accessOrderProtectedDeque().peekFirst();
    victimQueue = PROTECTED;
    continue;
  } else if (victimQueue == PROTECTED) {
    victim = accessOrderEdenDeque().peekFirst();
    victimQueue = EDEN;
    continue;
  }

  // The pending operations will adjust the size to reflect the correct weight
  break;
}
if ((victim != null) && (victim.getPolicyWeight() == 0)) {
  victim = victim.getNextInAccessOrder();
  continue;
} else if ((candidate != null) && (candidate.getPolicyWeight() == 0)) {
  candidate = candidate.getPreviousInAccessOrder();
  candidates--;
  continue;
}

首先,如果队首队尾都无法正常获取元素,则会变更淘汰处理的队列,变更顺序已经有提,并略过0权重下的数据。

if (victim == null) {
  candidates--;
  Node<K, V> evict = candidate;
  candidate = candidate.getPreviousInAccessOrder();
  evictEntry(evict, RemovalCause.SIZE, 0L);
  continue;
} else if (candidate == null) {
  Node<K, V> evict = victim;
  victim = victim.getNextInAccessOrder();
  evictEntry(evict, RemovalCause.SIZE, 0L);
  continue;
}

如果队首队尾元素有一个为null,那么就直接淘汰其中不为null的元素。

K victimKey = victim.getKey();
K candidateKey = candidate.getKey();
if (victimKey == null) {
  Node<K, V> evict = victim;
  victim = victim.getNextInAccessOrder();
  evictEntry(evict, RemovalCause.COLLECTED, 0L);
  continue;
} else if (candidateKey == null) {
  candidates--;
  Node<K, V> evict = candidate;
  candidate = candidate.getPreviousInAccessOrder();
  evictEntry(evict, RemovalCause.COLLECTED, 0L);
  continue;
}

之后,如果其中任意一个元素的key已经被回收,那么也直接就淘汰掉。

candidates--;
if (admit(candidateKey, victimKey)) {
  Node<K, V> evict = victim;
  victim = victim.getNextInAccessOrder();
  evictEntry(evict, RemovalCause.SIZE, 0L);
  candidate = candidate.getPreviousInAccessOrder();
} else {
  Node<K, V> evict = candidate;
  candidate = candidate.getPreviousInAccessOrder();
  evictEntry(evict, RemovalCause.SIZE, 0L);
}

最后,才采用admit()方法通过lfu算法进行淘汰,其中,lfu的具体实现在admit()方法中。

boolean admit(K candidateKey, K victimKey) {
  int victimFreq = frequencySketch().frequency(victimKey);
  int candidateFreq = frequencySketch().frequency(candidateKey);
  if (candidateFreq > victimFreq) {
    return true;
  } else if (candidateFreq <= 5) {
    // The maximum frequency is 15 and halved to 7 after a reset to age the history. An attack
    // exploits that a hot candidate is rejected in favor of a hot victim. The threshold of a warm
    // candidate reduces the number of random acceptances to minimize the impact on the hit rate.
    return false;
  }
  int random = ThreadLocalRandom.current().nextInt();
  return ((random & 127) == 0);
}

frequecy()方法来具体获得两个key的频度,其中具体的实现算法介绍在上一篇博文中已经提及。

如果队尾元素的频度大于队首,那么就直接淘汰队首,当队尾频度小于等于队首,且频度小于5的时候,直接将其淘汰,否则,通过随机的方式进行淘汰任意一个键。

这里的5的选择,与caffeine底层具体的lfu实现有关,因为底层通过4位记录一个频度,最大值是15,老的键可能由于超过15,导致频度减半变为7,所以队尾新加入的元素如果小于5会直接淘汰。

重复以上流程,淘汰到数据总量小于设定的最大容量。


版权声明:本文为weixin_40318210原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。