Caffeine使用一个ConcurrencyHashMap来保存所有数据,那它的过期淘汰策略采用什么方式与数据结构呢?其中写过期是使用writeOrderDeque,这个比较简单无需多说,而读过期相对复杂很多,使用W-TinyLFU的结构与算法。【资料获取】
网络上有很多文章介绍W-TinyLFU结构的,大家可以去查一下,这里主要是从源码来分析,总的来说它使用了三个双端队列:
-
accessOrderEdenDeque,accessOrderProbationDeque,accessOrderProtectedDeque,使用双端队列的原因是支持LRU算法比较方便。
-
accessOrderEdenDeque属于eden区,缓存1%的数据,其余的99%缓存在main区
-
accessOrderProbationDeque属于main区,缓存main内数据的20%,这部分是属于冷数据,即将补淘汰。
-
accessOrderProtectedDeque属于main区,缓存main内数据的80%,这部分是属于热数据,是整个缓存的主存区。
我们先看一下淘汰方法入口:
void evictEntries() {
if (!evicts()) {
return;
}
//先从edn区淘汰
int candidates = evictFromEden();
//eden淘汰后的数据进入main区,然后再从main区淘汰
evictFromMain(candidates);
}
accessOrderEdenDeque对应W-TinyLFU的W(window),这里保存的是最新写入数据的引用,它使用LRU淘汰,这里面的数据主要是应对突发流量的问题,淘汰后的数据进入accessOrderProbationDeque.代码如下:
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();
//先从eden区移除
accessOrderEdenDeque().remove(node);
//移除的数据加入到main区的probation队列
accessOrderProbationDeque().add(node);
candidates++;
lazySetEdenWeightedSize(edenWeightedSize() - node.getPolicyWeight());
}
node = next;
}
return candidates;
}
数据进入probation队列后,继续执行以下代码:
void evictFromMain(int candidates) {
int victimQueue = PROBATION;
Node<K, V> victim = accessOrderProbationDeque().peekFirst();
Node<K, V> candidate = accessOrderProbationDeque().peekLast();
while (weightedSize() > maximum()) {
// Stop trying to evict candidates and always prefer the victim
if (candidates == 0) {
candidate = null;
}
// Try evicting from the protected and eden queues
if ((candidate == null) && (victim == null)) {