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 = 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,只多了一次过期时间判断和相关状态的更新。

 

你可能感兴趣的:(caffeine)