Caffeine通过get()方法获取缓存中的数据。
Node node = data.get(nodeFactory.newLookupKey(key));
if (node == null) {
if (recordStats) {
statsCounter().recordMisses(1);
}
return null;
}
long now = expirationTicker().read();
if (hasExpired(node, now)) {
if (recordStats) {
statsCounter().recordMisses(1);
}
scheduleDrainBuffers();
return null;
}
首先将会通过neyLookupKey()构造用来搜索的key,如果是强引用的key那么直接就是调用时所传入的key,如果是弱引用的话将是一个继承自WeakReference的对象弱引用。
如果没有找到,记录到miss次数的统计中。
如果找到,在配置了过期时间的情况下,判断是否过期,如果过期也当做miss处理,并调用scheduleDrainBuffers()方法异步执行维护工作(在此处,如果异步维护任务在线程池中被拒绝,会直接同步执行过期的处理),整个同步取数据的流程结束返回null。
K castedKey = (K) key;
V value = node.getValue();
if (!isComputingAsync(node)) {
setVariableTime(node, expireAfterRead(node, castedKey, value, now));
setAccessTime(node, now);
}
afterRead(node, now, recordStats);
return value;
如果找到并且没有过期,在当前该元素没有竞争的情况下,更新该元素的剩余过期时间(会在后面重新根据时间加入时间轮)和访问时间(会在后面重新加入到访问队列末尾或升级队列)。最后通过afterRead()准备进行异步清理操作和更新判断(可能会触发所选取数据的更新),并直接返回结果。
因此,在caffeine中一次同步获取元素的流程(在没有由于过期并只能同步执行维护方法的前提下),在耗时中,相比原始访问map,只多了一次过期时间判断和相关状态的更新。
void afterRead(Node node, long now, boolean recordHit) {
if (recordHit) {
statsCounter().recordHits(1);
}
boolean delayable = skipReadBuffer() || (readBuffer.offer(node) != Buffer.FULL);
if (shouldDrainBuffers(delayable)) {
scheduleDrainBuffers();
}
refreshIfNeeded(node, now);
}
afterRead()中,首先在统计中记录命中操作,之后往readBuffer中添加针对该元素的read事件,这里的readBuffer会根据线程的threadlocal随机数选择线程专属的buffer,在写入事件中不会存在资源的竞争。(具体之前的文章有描述)
之后根据shouldDrainBuffers()方法判断,当前caffeine的异步维护任务是否正在执行,如果没有,则准备通过一开始提到的scheduleDrainBuffers()方法异步执行维护工作。
维护方法通过ForkJoinPool异步执行。
由元素访问而触发的维护方法在执行流程中主要分为6个主要过程(比write少一个add或者update的更新)。
drainReadBuffer();
drainWriteBuffer();
if (task != null) {
task.run();
}
drainKeyReferences();
drainValueReferences();
expireEntries();
evictEntries();
分别是获取所有readBuffer的read事件并执行相应的策略,获取writeBuffer(相比readBuffer只有一个)中的所有write事件执行,回收软弱引用的key和value,通过时间驱逐缓存中的元素,和根据lfu根据空间驱逐缓存中的元素。
在读操作中,主要关心readBuffer事件的处理。
void onAccess(Node node) {
if (evicts()) {
K key = node.getKey();
if (key == null) {
return;
}
frequencySketch().increment(key);
if (node.inEden()) {
reorder(accessOrderEdenDeque(), node);
} else if (node.inMainProbation()) {
reorderProbation(node);
} else {
reorder(accessOrderProtectedDeque(), node);
}
} else if (expiresAfterAccess()) {
reorder(accessOrderEdenDeque(), node);
}
if (expiresVariable()) {
timerWheel().reschedule(node);
}
}
在这里会异步更新该元素的lfu访问次数,并将该元素重新排到所在访问队列的末尾,如果是在probation队列中的元素,在足够的访问次数下,还会晋升到protect中,这部分在之前的文章有写。并根据该元素的剩余过期时间重新加入到时间轮中,时间轮的部分前面的文章也有写。
关于维护任务中的read部分暂且告一段落,回到之前 的afterRead()方法,会在最后调用refreshIfNeeded()方法,在这里会判断写入时间相比当前是否已经超出更新间隔,如果超出,将会通过异步方式重新执行元素的加载,重新加载获取新的值加入的缓存中。
重复之前的结论,在caffeine中一次同步获取元素的流程(在没有由于过期并维只能同步执行维护方法的前提下),在耗时中,相比原始访问map,只多了一次过期时间判断和相关状态的更新。