caffeine 读操作源码走读 为什么读这么快

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 = https://www.it610.com/article/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); } }

【caffeine 读操作源码走读 为什么读这么快】在这里会异步更新该元素的lfu访问次数,并将该元素重新排到所在访问队列的末尾,如果是在probation队列中的元素,在足够的访问次数下,还会晋升到protect中,这部分在之前的文章有写。并根据该元素的剩余过期时间重新加入到时间轮中,时间轮的部分前面的文章也有写。

关于维护任务中的read部分暂且告一段落,回到之前 的afterRead()方法,会在最后调用refreshIfNeeded()方法,在这里会判断写入时间相比当前是否已经超出更新间隔,如果超出,将会通过异步方式重新执行元素的加载,重新加载获取新的值加入的缓存中。

重复之前的结论,在caffeine中一次同步获取元素的流程(在没有由于过期并维只能同步执行维护方法的前提下),在耗时中,相比原始访问map,只多了一次过期时间判断和相关状态的更新。

    推荐阅读