Caffeine源码分析

前言

本章分析caffeine2.9.3版本的实现。

  1. 梳理通过不同api构造caffeine cache的分类
  2. 有界特性的实现方式,如自动刷新过期淘汰容量驱逐
  3. 其他边缘特性,如Metrics、Policy等;

一、同步和异步Cache

Caffeine构造Cache总共有5种build api,如下:

less 复制代码
/* sync api 2 **/
Cache<Object, Object> cache1 = Caffeine.newBuilder().build();
cache1.get(1, k -> null);

LoadingCache<Object, Object> cache2 = Caffeine.newBuilder().build(new CacheLoader<Object, Object>() {
    @Override
    public @Nullable Object load(@NonNull Object key) throws Exception {
        return key;
    }
});
cache2.get(1);

/* async api 3 **/
AsyncCache<Object, Object> cache3 = Caffeine.newBuilder().buildAsync();
CompletableFuture<Object> f3 = cache3.get(1, (k, v) -> CompletableFuture.supplyAsync(() -> null));

AsyncLoadingCache<Object, Object> cache4 = Caffeine.newBuilder().buildAsync(new CacheLoader<Object, Object>() {
    @Override
    public @Nullable Object load(@NonNull Object key) throws Exception {
        return key;
    }
});
CompletableFuture<Object> f4 = cache4.get(1);

AsyncLoadingCache<Object, Object> cache5 = Caffeine.newBuilder().buildAsync(new AsyncCacheLoader<Object, Object>() {
    @Override
    public @NonNull CompletableFuture<Object> asyncLoad(@NonNull Object key, @NonNull Executor executor) {
        return CompletableFuture.completedFuture(key);
    }
});
CompletableFuture<Object> f5 = cache5.get(1);

不同build方法,暴露给用户不同的cache的使用方式。

大方向上,Cache提供两套api:

  1. Cache ,Caffeine#build 构造返回,同步Cache实现,用户通过cache.get直接得到value;
  2. AsyncCache ,Caffeine#buildAsync 构造返回,异步AsyncCache实现,用户通过cache.get得到value的future;

如下面的类图显示,同步cache和异步cache是两套独立的接口和实现:

细节上,是否自动加载key对应value到cache又分为两种:

  1. 普通Cache ,用户build不传入CacheLoader ,cache miss仅支持通过get(key,mappingFunction)加载value或手动put
  2. LoadingCache ,用户build传入CacheLoader ,cache miss支持get(key)自动通过CacheLoader加载kv到cache

我们以同步cache为例,进行后续的分析

二、Cache骨架

1、Cache

Cache

Cache是暴露给用户的顶层api:

  1. 线程安全;
  2. 需要主动调用get/put方法存储数据;
  3. 支持手动清理;
less 复制代码
public interface Cache<K, V> {

  // 查询 key对应value 存在则返回
  @Nullable
  V getIfPresent(@NonNull @CompatibleWith("K") Object key);

  // 查询 key对应value
  // 如果key对应value不存在,使用mappingFunction查询value存储并返回
  // 对于同一个key,同一时间,mappingFunction只会有一个线程执行,其他线程被阻塞
  @Nullable
  V get(@NonNull K key, @NonNull Function<? super K, ? extends V> mappingFunction);

  // 存储 key对应value
  void put(@NonNull K key, @NonNull V value);

  // 清理 key对应value
  void invalidate(@NonNull @CompatibleWith("K") Object key);

  // 查询 目前kv数量
  @NonNegative
  long estimatedSize();

  // 获取底层线程安全map视图,thread-safe,能使用所有map方法
  // 所有对map的写,都直接影响cache
  @NonNull
  ConcurrentMap<@NonNull K, @NonNull V> asMap();

  // 暴露给用户可操作底层特性的api
  @NonNull
  Policy<K, V> policy();
}

LocalManualCache

LocalManualCache 继承Cache接口,提供对Cache的骨架实现,大部分方法都直接委派给cache()方法返回的LocalCache。所以真正的实现在LocalCache中。

typescript 复制代码
interface LocalManualCache<K, V> extends Cache<K, V> {
  /** Returns the backing {@link LocalCache} data store. */
  LocalCache<K, V> cache();
  @Override
  default long estimatedSize() {
    return cache().estimatedSize();
  }
  @Override
  default @Nullable V getIfPresent(Object key) {
    return cache().getIfPresent(key, /* recordStats */ true);
  }
  @Override
  default @Nullable V get(K key, Function<? super K, ? extends V> mappingFunction) {
    return cache().computeIfAbsent(key, mappingFunction);
  }
  @Override
  default void put(K key, V value) {
    cache().put(key, value);
  }
  @Override
  default void invalidate(Object key) {
    cache().remove(key);
  }
  @Override
  default ConcurrentMap<K, V> asMap() {
    return cache();
  }
}

2、LoadingCache

LoadingCache

如果用户在Caffeine#build时,传入CacheLoader ,将构建LoadingCache实现。

LoadingCache在Cache的基础上,提供了cache miss通过CacheLoader加载value的能力

  • get(key):查询key对应缓存,如果key对应value不存在,通过CacheLoader查询存入cache并返回。和Cache.get(key,mappingFunction)一样,同一时间同一key,只允许一个线程执行CacheLoader刷缓存,其他线程需要等待这个线程执行完毕后才能进入;
  • refresh(key):异步刷新key对应缓存,如果CacheLoader异常仅打印日志,不会操作key对应value;如果key对应value存在,则调用CacheLoader#reload;如果key对应value不存在,则调用CacheLoader#load;
less 复制代码
public interface LoadingCache<K, V> extends Cache<K, V> {
  @Nullable
  V get(@NonNull K key);

  void refresh(@NonNull K key);
}

LocalLoadingCache

LocalLoadingCache是LoadingCache的骨架实现:

  1. 继承Cache骨架LocalManualCache能获取cache方法返回的LocalCache,操作底层cache;
  2. 依赖CacheLoader,用户加载缓存逻辑;
swift 复制代码
interface LocalLoadingCache<K, V> extends LocalManualCache<K, V>, LoadingCache<K, V> {

  /** Returns the {@link CacheLoader} used by this cache. */
  CacheLoader<? super K, V> cacheLoader();
}

LocalLoadingCache#get :查询缓存。同LocalManualCache#get(key,mappingFunction),CacheLoader#load方法转换为普通Function,适配底层LocalCache通用computeIfAbsent方法;

csharp 复制代码
interface LocalLoadingCache<K, V> extends LocalManualCache<K, V>, LoadingCache<K, V> {

  /** Returns the {@link CacheLoader#load} as a mapping function. */
  Function<K, V> mappingFunction();

  // get方法实现,同LocalManualCache#get(key,mappingFunction)
  @Override
  default @Nullable V get(K key) {
    return cache().computeIfAbsent(key, mappingFunction());
  }
}

LocalLoadingCache#refresh主动触发异步刷新缓存 (逻辑与refreshAfterWrite自动刷新特性类似)。依赖LocalCache和CacheLoader

  1. 查询老缓存oldValue;
  2. 根据oldValue是否存在,调用CacheLoader的load或reload方法,异步加载newValue;这一步发生异常,不更新缓存;
  3. 比较当前缓存currentValue和oldValue和newValue的关系,决定最终存储哪个value;
  4. 如果newValue被丢弃,通知listener;
scss 复制代码
@Override
default void refresh(K key) {
    requireNonNull(key);
    long[] writeTime = new long[1];
    // 查询key对应value,记录value写入时间writeTime
    V oldValue = cache().getIfPresentQuietly(key, writeTime);
    // 调用CacheLoader异步加载缓存
    CompletableFuture<V> refreshFuture = (oldValue == null)
        ? cacheLoader().asyncLoad(key, cache().executor())
        : cacheLoader().asyncReload(key, oldValue, cache().executor());
    refreshFuture.whenComplete((newValue, error) -> {
    if (error != null) {
        // 业务异常,仅打印日志
        if (!(error instanceof CancellationException) && !(error instanceof TimeoutException)) {
          logger.log(Level.WARNING, "Exception thrown during refresh", error);
        }
        // 发生异常,什么都不做,直接返回
        return;
      }
    
    boolean[] discard = new boolean[1]; // 刷新缓存,也有可能被丢弃
    cache().compute(key, (k, currentValue) -> {
    if (currentValue == null) {
      // case1 当前缓存为空,直接设置new缓存
      return newValue;
    } else if (currentValue == oldValue) {
      // case2 当前缓存 == 老缓存,即缓存在异步刷新期间,未发生变化
      long expectedWriteTime = writeTime[0];
      if (cache().hasWriteTime()) {
        cache().getIfPresentQuietly(key, writeTime);
      }
      if (writeTime[0] == expectedWriteTime) {
         // 再次确认,当前缓存写入时间,和老缓存写入时间,一致,则更新为新缓存
        return newValue;
      }
    }
      // case3 刷新期间,缓存已发生变化,保持当前缓存不变
      discard[0] = true; 
      return currentValue;
    }, /* recordMiss */ false, /* recordLoad */ false, /* recordLoadFailure */ true);
    // 刷新缓存被丢弃,通知listener
    if (discard[0] && cache().hasRemovalListener()) {
      cache().notifyRemoval(key, newValue, RemovalCause.REPLACED);
    }
  //...
});

CacheLoader

CacheLoader接口定义了两个方法:

  1. load:加载key对应value,允许返回空,代表value不存在,允许抛出异常,保持底层缓存不变;
  2. reload:特殊的load方法,仅当刷新缓存时key对应oldValue存在时执行,默认实现直接调用普通load方法;
less 复制代码
public interface CacheLoader<K, V> extends AsyncCacheLoader<K, V> {
  @Nullable
  V load(@NonNull K key) throws Exception;

  @Nullable
  default V reload(@NonNull K key, @NonNull V oldValue) throws Exception {
    return load(key);
  }
}

CacheLoader继承AsyncCacheLoader ,提供了异步刷新缓存的默认实现。

asyncLoad/asyncReload使用指定Executor异步刷新缓存返回future。

less 复制代码
public interface CacheLoader<K, V> extends AsyncCacheLoader<K, V> {
  @Override @NonNull
  default CompletableFuture<V> asyncLoad(@NonNull K key, @NonNull Executor executor) {
    requireNonNull(key);
    requireNonNull(executor);
    return CompletableFuture.supplyAsync(() -> {
      try {
        return load(key);
      } catch (RuntimeException e) {
        throw e;
      } catch (Exception e) {
        throw new CompletionException(e);
      }
    }, executor);
  }
  @Override @NonNull
  default CompletableFuture<V> asyncReload(
      @NonNull K key, @NonNull V oldValue, @NonNull Executor executor) {
    requireNonNull(key);
    requireNonNull(executor);
    return CompletableFuture.supplyAsync(() -> {
      try {
        return reload(key, oldValue);
      } catch (RuntimeException e) {
        throw e;
      } catch (Exception e) {
        throw new CompletionException(e);
      }
    }, executor);
  }
}

三、Cache实现

1、Cache实现的分类

最终暴露给用户的Cache实现一共有四种:

提供CacheLoader,LoadingCache支持手动refresh刷新缓存,支持get api自动加载缓存。

是否有界,决定了Cache的底层实现LocalCache。

实现 是否有界(Bounded/Unbounded) 是否提供CacheLoader(Loading/Manual)
BoundedLocalManualCache
BoundedLocalLoadingCache
UnboundedLocalManualCache
UnboundedLocalLoadingCache

2、有界or无界

Caffeine#build() :无CacheLoader,即ManualCache,是否有界取决于Caffeine#isBounded。

swift 复制代码
// Caffeine.newBuilder()....build();
public <K1 extends K, V1 extends V> Cache<K1, V1> build() {
    // ...
    Caffeine<K1, V1> self = (Caffeine<K1, V1>) this;
    return isBounded()
        ? new BoundedLocalCache.BoundedLocalManualCache<>(self)
        : new UnboundedLocalCache.UnboundedLocalManualCache<>(self);
}

Caffeine#isBounded:有界情况。如果用户配置了,如:maximumXXX最大容量、expireXXX过期时间、key/value强度(弱引用、软引用)。

yaml 复制代码
 boolean isBounded() {
    return (maximumSize != UNSET_INT)
        || (maximumWeight != UNSET_INT)
        || (expireAfterAccessNanos != UNSET_INT)
        || (expireAfterWriteNanos != UNSET_INT)
        || (expiry != null)
        || (keyStrength != null)
        || (valueStrength != null);
  }

Caffeine#build(CacheLoader)

有CacheLoader,即LoadingCache,除了普通有界情况之外,如果满足refreshAfterWrite,也认为有界。

即如果配置refreshAfterWrite自动刷新缓存,无论是否单纯有界,都构造有界实现。

typescript 复制代码
@NonNull
public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build(
  @NonNull CacheLoader<? super K1, V1> loader) {
    // ...
    Caffeine<K1, V1> self = (Caffeine<K1, V1>) this;
    return isBounded() || refreshAfterWrite()
        ? new BoundedLocalCache.BoundedLocalLoadingCache<>(self, loader)
        : new UnboundedLocalCache.UnboundedLocalLoadingCache<>(self, loader);
}
boolean refreshAfterWrite() {
	return refreshAfterWriteNanos != UNSET_INT;
}

总的来说,大部分情况下都使用有界缓存。

3、无界

UnboundedLocalManualCache手动Cache。

实现cache方法返回UnboundedLocalCache,其他都交给LocalManualCache骨架实现。

java 复制代码
static class UnboundedLocalManualCache<K, V> implements LocalManualCache<K, V>, Serializable {
    private static final long serialVersionUID = 1;

    final UnboundedLocalCache<K, V> cache;
    @Nullable Policy<K, V> policy;

    UnboundedLocalManualCache(Caffeine<K, V> builder) {
      cache = new UnboundedLocalCache<>(builder, /* async */ false);
    }

    @Override
    public UnboundedLocalCache<K, V> cache() {
      return cache;
    }

    @Override
    public Policy<K, V> policy() {
      return (policy == null)
          ? (policy = new UnboundedPolicy<>(cache, Function.identity()))
          : policy;
    }
}

UnboundedLocalLoadingCache自动Cache。

继承手动Cache,实现LocalLoadingCache自动Cache骨架,提供用户CacheLoader。

typescript 复制代码
static final class UnboundedLocalLoadingCache<K, V> 
    extends UnboundedLocalManualCache<K, V> implements LocalLoadingCache<K, V> {

    final Function<K, V> mappingFunction;
    final CacheLoader<? super K, V> loader;

    UnboundedLocalLoadingCache(Caffeine<K, V> builder, CacheLoader<? super K, V> loader) {
      super(builder);
      this.loader = loader;
      this.mappingFunction = newMappingFunction(loader);
    }

    @Override
    public CacheLoader<? super K, V> cacheLoader() {
      return loader;
    }

    @Override
    public Function<K, V> mappingFunction() {
      return mappingFunction;
    }
}

无论自动还是手动,底层实现都是UnboundedLocalCache ,本质上包装了ConcurrentHashMap

kotlin 复制代码
final class UnboundedLocalCache<K, V> implements LocalCache<K, V> {
  final RemovalListener<K, V> removalListener;
  final ConcurrentHashMap<K, V> data;
  final Executor executor;
  // ...
}

UnboundedLocalCache,增删改查,省略一些特性实现,实际底层都是委派给ConcurrentHashMap实现

typescript 复制代码
@Override
public @Nullable V getIfPresent(Object key, boolean recordStats) {
    V value = data.get(key);
    // ...
    return value;
}
@Override
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction,
  boolean recordStats, boolean recordLoad) {
    requireNonNull(mappingFunction);
    V value = data.get(key);
    if (value != null) {
      return value;
    }
     // 缓存不存在,通过mappingFunction(CacheLoader)加载
    value = data.computeIfAbsent(key, k -> {
      return mappingFunction.apply(key);
    });
    return value;
}

@Override
public @Nullable V put(K key, V value, boolean notifyWriter) {
    requireNonNull(value);
    V[] oldValue = (V[]) new Object[1];
    data.compute(key, (k, v) -> {
        oldValue[0] = v;
        return value;
    });
    // ...
    return oldValue[0];
}

4、有界

概览

和无界类似,暴露给用户的Cache实现有两种:

  1. BoundedLocalManualCache:手动Cache;
  2. BoundedLocalLoadingCache:自动Cache,支持CacheLoader;
java 复制代码
static class BoundedLocalManualCache<K, V> implements LocalManualCache<K, V>, Serializable {
    private static final long serialVersionUID = 1;

    final BoundedLocalCache<K, V> cache;
    final boolean isWeighted;

    BoundedLocalManualCache(Caffeine<K, V> builder) {
      this(builder, null);
    }

    BoundedLocalManualCache(Caffeine<K, V> builder, @Nullable CacheLoader<? super K, V> loader) {
      cache = LocalCacheFactory.newBoundedLocalCache(builder, loader, /* async */ false);
      isWeighted = builder.isWeighted();
    }

    @Override
    public BoundedLocalCache<K, V> cache() {
      return cache;
    }
}
static final class BoundedLocalLoadingCache<K, V>
      extends BoundedLocalManualCache<K, V> implements LocalLoadingCache<K, V> {
    private static final long serialVersionUID = 1;

    final Function<K, V> mappingFunction;

    BoundedLocalLoadingCache(Caffeine<K, V> builder, CacheLoader<? super K, V> loader) {
      super(builder, loader);
      requireNonNull(loader);
      mappingFunction = newMappingFunction(loader);
    }

    @Override
    @SuppressWarnings("NullAway")
    public CacheLoader<? super K, V> cacheLoader() {
      return cache.cacheLoader;
    }

    @Override
    public Function<K, V> mappingFunction() {
      return mappingFunction;
    }
}

无论哪种Cache,都委派给BoundedLocalCache处理。

BoundedLocalCache

BoundedLocalCache 是个抽象类 ,底层还是用一个ConcurrentHashMap来维护缓存数据。

有界Cache在ConcurrentMap的value中不单纯用用户原始value存储,而是包装为一个Node

scala 复制代码
abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef<K, V>
    implements LocalCache<K, V> {
    final ConcurrentHashMap<Object, Node<K, V>> data;
}

DrainStatusRefBoundedLocalCache父类,维护了一个状态drainStatus。

scala 复制代码
abstract static class DrainStatusRef<K, V> extends PadDrainStatus<K, V> {
    /** The draining status of the buffers. */
    volatile int drainStatus = IDLE;
}

PadDrainStatusDrainStatusRef 的父类,解决伪共享问题,帮DrainStatusRef中的volatile drainStatus填充。

scala 复制代码
abstract static class PadDrainStatus<K, V> extends AbstractMap<K, V> {
    byte p000, p001, p002, p003, p004, p005, p006, p007;
    // ... p008 - p111
    byte p112, p113, p114, p115, p116, p117, p118, p119;
}

Node

Node是一个抽象类

Node包含自己基础的方法,比如key/value/weight的getter和setter,默认所有node的weight都是1。

less 复制代码
abstract class Node<K, V> implements AccessOrder<Node<K, V>>, WriteOrder<Node<K, V>> {
  @Nullable
  public abstract K getKey();
  @NonNull
  public abstract Object getKeyReference();
  @Nullable
  public abstract V getValue();
  @NonNull
  public abstract Object getValueReference();
  public abstract void setValue(@NonNull V value, @Nullable ReferenceQueue<V> referenceQueue);
  @NonNegative
  public int getWeight() {
    return 1;
  }
  public void setWeight(@NonNegative int weight) {}
}

如果有界特性与缓存访问(access)情况有关:

  1. 容量限制 :W-TinyLFU,需要实现AccessOrder让Node按照访问顺序形成链表;
  2. expireAfterAccess:需要记录访问时间accessTime;
java 复制代码
/* --------------- Access order --------------- */
  public static final int WINDOW = 0;
  public static final  int PROBATION = 1;
  public static final  int PROTECTED = 2;
  // ...
  public int getQueueType() {
    return WINDOW;
  }
  public void setQueueType(int queueType) {
    throw new UnsupportedOperationException();
  }
  public long getAccessTime() {
    return 0L;
  }
  public void setAccessTime(long time) {}
  @Override
  public @Nullable Node<K, V> getPreviousInAccessOrder() {
    return null;
  }
  @Override
  public void setPreviousInAccessOrder(@Nullable Node<K, V> prev) {
    throw new UnsupportedOperationException();
  }

  @Override
  public @Nullable Node<K, V> getNextInAccessOrder() {
    return null;
  }

  @Override
  public void setNextInAccessOrder(@Nullable Node<K, V> next) {
    throw new UnsupportedOperationException();
  }

如果有界特性与缓存写入(write)情况有关:

  1. refreshAfterWrite:需要记录写入时间writeTime;
  2. expireAfterWrite :需要记录写入时间writeTime,需要实现WriteOrder让Node按照写入顺序形成链表;
typescript 复制代码
/* --------------- Write order --------------- */
  public long getWriteTime() {
    return 0L;
  }
  public void setWriteTime(long time) {}
  public boolean casWriteTime(long expect, long update) {
    throw new UnsupportedOperationException();
  }

  @Override
  public @Nullable Node<K, V> getPreviousInWriteOrder() {
    return null;
  }

  @Override
  public void setPreviousInWriteOrder(@Nullable Node<K, V> prev) {
    throw new UnsupportedOperationException();
  }

  @Override
  public @Nullable Node<K, V> getNextInWriteOrder() {
    return null;
  }

  @Override
  public void setNextInWriteOrder(@Nullable Node<K, V> next) {
    throw new UnsupportedOperationException();
  }

javapoet

有界缓存的两个核心类实现都是通过javapoet框架通过编码方式生成的:

  1. BoundedLocalCache抽象类的实现 ,通过LocalCacheFactory工厂获取;
  2. Node抽象类的实现 ,通过NodeFactory工厂获取;

LocalCacheFactory#newBoundedLocalCache:简单工厂。

根据用户对Caffeine的配置情况,反射构造BoundedLocalCache实现类。

less 复制代码
static <K, V> BoundedLocalCache<K, V> newBoundedLocalCache(
      Caffeine<K, V> builder, @Nullable CacheLoader<? super K, V> cacheLoader, boolean async) {
    StringBuilder sb = new StringBuilder("com.github.benmanes.caffeine.cache.");
    // ... 其他特性
    if (builder.evicts()) {
      sb.append('M');
      if (builder.isWeighted()) {
        sb.append('W');
      } else {
        sb.append('S');
      }
    }
    if (builder.expiresAfterAccess() || builder.expiresVariable()) {
      sb.append('A');
    }
    if (builder.expiresAfterWrite()) {
      sb.append('W');
    }
    if (builder.refreshAfterWrite()) {
      sb.append('R');
    }
    try {
      Class<?> clazz = Class.forName(sb.toString());
      Constructor<?> ctor =
          clazz.getDeclaredConstructor(Caffeine.class, CacheLoader.class, boolean.class);
      @SuppressWarnings("unchecked")
      BoundedLocalCache<K, V> factory =
          (BoundedLocalCache<K, V>) ctor.newInstance(builder, cacheLoader, async);
      return factory;
    } catch (ReflectiveOperationException e) {
      throw new IllegalStateException(sb.toString(), e);
    }
  }

BoundedLocalCache 实现类通过代码生成的方式,减少需要的字段和执行路径

Node同理根据用户配置生成不同NodeFactory,见NodeFactory#newFactory。

Node 通过代码生成是为了减少内存使用

常见方法

先简单分析(忽略各种特性)一下BoundedLocalCache的常用两个常用方法。

getIfPresent

用户使用有界Cache,调用Cache#getIfPresent方法,底层会委派给BoundedLocalCache#getIfPresent。

scss 复制代码
Cache<Object, Object> cache = Caffeine.newBuilder().maximumSize(1000).build();
cache.getIfPresent(1);// cache miss -> do nothing

BoundedLocalCache#getIfPresent:忽略特性,有三种情况

  1. key对应node不存在,直接返回null;
  2. key对应node存在,但是过期(expire)或value被gc(soft/weakValues),返回null;
  3. 正常返回缓存值;

可以看到,查询时会涉及缓存过期判断。即过期缓存即使还未清理,也不会返回给用户。

kotlin 复制代码
final ConcurrentHashMap<Object, Node<K, V>> data;
public @Nullable V getIfPresent(Object key, boolean recordStats) {
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key)/*key特性*/);
    // case1 node为空,缓存不存在,返回null
    if (node == null) {
       // ...
      return null;
    }
    // case2 node非空,value 过期/被gc,返回null
    V value = node.getValue();
    long now = expirationTicker().read();
    if (hasExpired(node, now)/*expire特性*/ 
        || (collectValues() && (value == null))/*value特性*/) {
      // ...
      return null;
    }
    // case3 node非空,value 正常,返回value
    // ...
    return value;
}

computeIfAbsent

用户使用LoadingCache#get(key)或Cache#get(key,mappingFunction),底层都会调用BoundedLocalCache#computeIfAbsent方法。

less 复制代码
LoadingCache<Object, Object> cache = Caffeine.newBuilder().maximumSize(1000)
			.build(new CacheLoader<Object, Object>() {
                @Override
                public @Nullable Object load(@NonNull Object key) throws Exception {
                    return null;
                }
            });
cache.get(1);// cache miss -> cache loader
// cache miss -> mappingFunction
cache.get(1, new Function<Object, Object>() {
    @Override
    public Object apply(Object key) {
        return null;
    }
});

BoundedLocalCache#computeIfAbsent

  1. fast-path,如果data中存在未过期node或未gc的value,直接返回缓存,这是一次无锁普通读(ConcurrentHashMap无锁);
  2. 其他情况,即没有合适的缓存,需要走有锁逻辑doComputeIfAbsent,可能需要写;
typescript 复制代码
final ConcurrentHashMap<Object, Node<K, V>> data;

public @Nullable V computeIfAbsent(K key, 
      Function<? super K, ? extends V> mappingFunction,
      boolean recordStats, boolean recordLoad) {
    // Case1 fast-path 无锁 key对应value未gc且未过期,返回value
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key)/*weakKey特性*/);
    if (node != null) {
      V value = node.getValue();
      if ((value != null)/*value特性*/ && !hasExpired(node, now)/*expire特性*/) {
        // ...
        return value;
      }
    }
    /*weakKey特性*/
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
    // Case2 其他情况,如 node不存在/value过期
    return doComputeIfAbsent(key, keyRef, mappingFunction, new long[] { now }, recordStats);
}

BoundedLocalCache#doComputeIfAbsent:key对应缓存不存在/过期的情况,可能需要写

  1. S1,利用ConcurrentHashMap的compute方法,获取key级别锁
  2. S2,如果当前缓存Node不存在,执行用户查询逻辑(mappingFunction/CacheLoader)
    1. 查询返回为空,什么都不做;
    2. 查询返回非空,封装为Node放入data;
  1. S3,如果当前缓存Node存在获取Node级别锁
    1. 如果node未过期,value未gc,直接返回这个node,这是一次普通读;
    2. 否则执行用户查询逻辑;
    3. 如果用户查询结果为null,移除缓存Node;如果非空,更新缓存Node;
  1. S4,根据S2和S3的执行情况,执行各种特性处理,返回Node的value;
scss 复制代码
V doComputeIfAbsent(K key, Object keyRef,
      Function<? super K, ? extends V> mappingFunction, long[] now, boolean recordStats) {
    V[] oldValue = (V[]) new Object[1];
    V[] newValue = (V[]) new Object[1];
    K[] nodeKey = (K[]) new Object[1];
    Node<K, V>[] removed = new Node[1];

    int[] weight = new int[2]; // old, new
    RemovalCause[] cause = new RemovalCause[1];
    // S1,key锁(concurrentHashMap)
    Node<K, V> node = data.compute(keyRef, (k, n) -> {
      // S2 case1 node不存在
      if (n == null) {
        // S2-1,用户查询逻辑
        newValue[0] = mappingFunction.apply(key);
        if (newValue[0] == null) { // 用户返回null
          return null;
        }
        // S2-2,特性处理,返回新node
        now[0] = expirationTicker().read(); /*ticker特性*/
        weight[1] = weigher.weigh(key, newValue[0]); /*weigher特性*/
        n = nodeFactory.newNode(key, keyReferenceQueue() /*key特性*/,
            newValue[0], valueReferenceQueue() /*value特性*/, weight[1], now[0]);
        setVariableTime(n, expireAfterCreate(key, newValue[0], expiry(), now[0]));/*expire特性*/
        return n; // data注入新node
      }

      // S3 case2 node存在,node锁
      synchronized (n) {
        // S3-1,非 过期、弱key、软/弱value 普通读 返回node
        nodeKey[0] = n.getKey();
        weight[0] = n.getWeight();
        oldValue[0] = n.getValue();
        if ((nodeKey[0] == null) || (oldValue[0] == null)) { /*key/value特性*/
          cause[0] = RemovalCause.COLLECTED; // gc
        } else if (hasExpired(n, now[0])) { /*expire特性*/
          cause[0] = RemovalCause.EXPIRED; // expired
        } else {
          return n;
        }

        // S3-2、用户逻辑
        newValue[0] = mappingFunction.apply(key);

        // S3-3-1、如果新value为null,移除老node
        if (newValue[0] == null) {
          removed[0] = n;
          n.retire();
          return null;
        }

        // S3-3-2、如果新value不为null,更新node
        weight[1] = weigher.weigh(key, newValue[0]);
        n.setValue(newValue[0], valueReferenceQueue()); /*value特性*/
        n.setWeight(weight[1]); /*weigher特性*/

        now[0] = expirationTicker().read(); /*ticker特性*/
        setVariableTime(n, expireAfterCreate(key, newValue[0], expiry(), now[0]));/*expire特性*/
        setAccessTime(n, now[0]);/*expire特性*/
        setWriteTime(n, now[0]);/*expire/refresh特性*/
        return n;
      }
    });

    // S4 出key/node锁,后续特性处理
    // S4-1 用户mappingFunction返回null,返回null
    if (node == null) {
      // 特性处理...
      return null;
    }
    // 特性处理...
    // S4-2 老value未过期/gc,这是一次普通读,返回老value
    if (newValue[0] == null) {
       // 特性处理...
      return oldValue[0];
    }
    // 特性处理...
    // S4-3 老value过期/gc,用户mappingFunction返回非null,返回新value
    return newValue[0];
  }

总的来说,computeIfAbsent利用了ConcurrentHashMap的compute方法对缓存key纬度上锁 ,又因为要提供一些有界缓存需要的特性,需要对key对应Node上锁 ,在这中间穿插了许多有界缓存特性处理。

四、有界特性

有界特性大致分为四类:

  1. 最大容量策略(maximum):基于总容量size(权重是1的特例)、基于权重weight;
  2. 过期策略(expire):写后过期write、读写后过期access、自定义Expiry过期;
  3. key/value策略:软value、弱key、弱value;
  4. 自动刷新策略:使用LoadingCache的情况下,写后自动刷新缓存策略;

这里面有些特性就忽略了,要么是用的少(自定义expiry、弱key/value),要么是作者也不建议用(软value)。

scss 复制代码
LoadingCache<Object, Object> cache = Caffeine.newBuilder()
            // removal
            // RemovalCause - SIZE (evict)
            .maximumSize(1000)
            .maximumWeight(1000)
            .weigher(new Weigher() {})
            // RemovalCause - EXPIRED (expire)
            .expireAfterWrite(Duration.ofHours(1))
            .expireAfterAccess(Duration.ofHours(1))
            .expireAfter(new Expiry<Object, Object>() {})
            // RemovalCause - COLLECTED
            .softValues()
            .weakKeys()
            .weakValues()
            // refresh
            .refreshAfterWrite(Duration.ofHours(1))
            // LoadingCache
            .build(new CacheLoader<Object, Object>() {});

1、自动刷新

refreshAfterWrite的触发场景是一次有效读 ,即缓存存在且有效(或未被gc,如弱key等情况)。

比如get/getIfPresent都会触发,但是触发本次刷新的查询,依旧会返回旧缓存值

less 复制代码
LoadingCache<Integer, String> cache = Caffeine.newBuilder()
    .expireAfterWrite(Duration.ofSeconds(10)) // 10s后过期
    .refreshAfterWrite(Duration.ofNanos(1)) // 1ns后自动刷新
    .build(new CacheLoader<Integer, String>() {
        @Override
        public @Nullable String load(@NonNull Integer key) throws Exception {
            String value = UUID.randomUUID().toString();
            System.out.println(Thread.currentThread().getName() + ",load," + key + "," + value);
            return value;
        }
    });
 // 缓存存在,且未过期
cache.put(1, "1");
TimeUnit.NANOSECONDS.sleep(3);
 // 超过refreshAfterWrite,异步刷新,但是返回老value=1
System.out.println(cache.get(1));

BoundedLocalCache#afterRead

每次成功读到有效缓存Node,都会走afterRead方法,最后处理refresh逻辑。

BoundedLocalCache#refreshIfNeeded:

Step1,cas(node.writeTime, 当前时间+220年)确保单线程刷新

注:这也会让expireAfterWrite失效,不会让node过期。

Step2,提交查询任务到executor线程池,执行用户CacheLoader#reload。

如果reload异常,打印日志,cas恢复原始writeTime;

如果reload正常:

  1. 如果reload期间,value未发生变化,设置为reload返回value;
  2. 反之,保持value不变;

BoundedLocalCache#remap:

compute方法最终还是会拿key/node级别锁更新底层data(ConcurrentHashMap)。

2、有界特性底层支持

除了自动刷新,其他特性都需要以下支持。

Mpsc

在用户调用Cache的读写方法时(如get/put),用户线程只会负责记录Node的访问时间或写入时间。

比如getIfPresent一个单纯的读方法,如果命中缓存,记录访问时间,并将Node放入一个readBuffer

比如computeIfAbsent,如果发生写,记录时间,提交不同类型的任务到writeBuffer

scss 复制代码
@Nullable V doComputeIfAbsent(K key, Object keyRef,
      Function<? super K, ? extends V> mappingFunction, long[] now, boolean recordStats) {
    // S1,key锁(concurrentHashMap)
    Node<K, V> node = data.compute(keyRef, (k, n) -> {
      // S2 case1 value不存在
      if (n == null) {
          // ...创建node
          return new Node();
      }
      // S3 case2 value存在,node锁
      synchronized (n) {
          // n存在但是过期/gc,更新node
        n.setValue(newValue[0], valueReferenceQueue());
        setVariableTime(n, expireAfterCreate(key, newValue[0], expiry(), now[0]));/*expire特性*/
        setAccessTime(n, now[0]);/*expire特性*/
        setWriteTime(n, now[0]);/*expire/refresh特性*/
        return n;
      }
    });
    // S4 出key/node锁,后续特性处理
    // ...
    if ((oldValue[0] == null) && (cause[0] == null)) {
      // 新增缓存key-node
      afterWrite(new AddTask(node, weight[1]));
    } else {
      // 更新缓存key-node
      int weightedDifference = (weight[1] - weight[0]);
      afterWrite(new UpdateTask(node, weightedDifference));
    }
    return newValue[0];
  }

void afterWrite(Runnable task) {
    for (int i = 0; i < WRITE_BUFFER_RETRIES; i++) {
      // 写writeBuffer
      if (writeBuffer.offer(task)) {
         // 按需唤起maintenance
        scheduleAfterWrite();
        return;
      }
      scheduleDrainBuffers();
    }
    // ...
}

用户线程只会记录时间,将读写事件放入内存buffer,交给一个maintenance线程做后续处理。

其中readBuffer和writeBuffer都是mpscmultiple producer single consumer)的一种实现,无锁都是cas。

ReadBuffer

readBuffer的实现是BoundedBuffer,不保证有序(因为有个hash):

  1. RingBuffer:底层环形数组,容量16,有填充(ReadAndWriteCounterRef)防止伪共享问题;
  2. StripedBuffer:和juc原子累加器(如LongAdder)一样的思想,组合多个RingBuffer,尽量避免多线程cas一个RingBuffer;
scala 复制代码
final class BoundedBuffer<E> extends StripedBuffer<E> {
    @Override
    protected Buffer<E> create(E e) {
    	return new RingBuffer<>(e);
    }

    static final class RingBuffer<E> 
        extends BBHeader.ReadAndWriteCounterRef implements Buffer<E> {
    	final AtomicReferenceArray<E> buffer;
    }
}
abstract class StripedBuffer<E> implements Buffer<E> {
    transient volatile Buffer<E> @Nullable[] table;
}

WriteBuffer

WriteBuffer的实现是MpscGrowableArrayQueue ,copy自JCTools,大致特点:

  1. 多个2^n+1容量数组组成,每个数组的最后一个元素,存储下一个数组的开始位置;
  2. 前2^n-1个位置存储实际元素,2^n位置存储标志JUMP;
  3. 生产者多线程,操作生产下标,需要cas,如果超出容量,需要扩容,按照当前数组的两倍扩容;
  4. 消费者单线程,操作消费下标,不需要cas,但是要保证可见性(A线程和B线程不同时消费,但是A消费完可以切换B消费),如果遇到JUMP跳转到下一个数组;
  5. 在Caffeine中,初始容量是4,最大容量是(128*核数)取最接近的2n次幂,如果producer入队持续失败,会让用户线程处理consumer任务;

maintenance任务

BoundedLocalCache#scheduleDrainBuffers

用户线程读写缓存都可能拉起一个maintenance任务,用于消费buffer,处理有界特性。

注意,这里用的executor是用户构建Cache时指定的,未指定就是ForkJoinPool.commonPool

如果executor提交报错,将会降级由用户线程处理buffer

对于写请求,一定需要执行任务;

对于读请求,满足某些条件的情况下(比如读buffer比较满),必须要执行任务;

无论如何,通过几个状态位控制同时只有一个线程执行任务,保证single consumer,后续逻辑处理就都无锁了。

PerformCleanupTask执行maintenance。

BoundedLocalCache#performCleanUp:因为暴露了手动cleanup入口,所以加了个锁。

BoundedLocalCache#maintenance:有界特性的核心方法。

  1. drainReadBuffer:处理读buffer;
  2. drainWriteBuffer:处理写buffer;
  3. drainKeyReferences:处理弱key,忽略;
  4. drainValueReferences:处理软value/弱value,忽略;
  5. expireEntries:处理过期;
  6. evictEntries:处理最大容量驱逐;
  7. climb:最大容量策略下,动态参数调整;
scss 复制代码
void maintenance(@Nullable Runnable task) {
    lazySetDrainStatus(PROCESSING_TO_IDLE);
    try {
      drainReadBuffer();
      drainWriteBuffer();
      drainKeyReferences();
      drainValueReferences();
      expireEntries();
      evictEntries();
      climb();
    } finally {
      if ((drainStatus() != PROCESSING_TO_IDLE) || !casDrainStatus(PROCESSING_TO_IDLE, IDLE)) {
        lazySetDrainStatus(REQUIRED);
      }
    }
}

3、容量驱逐

使用上最大容量支持两种方式,在底层实现上maximumSize可以认为是权重的特例,每个kv权重都是1。

less 复制代码
Cache<Object, Object> cache2 = Caffeine.newBuilder()
.maximumSize(max).build();

Cache<Object, Object> cache3 = Caffeine.newBuilder()
.maximumWeight(max)
.weigher(new Weigher<Object, Object>() {
    @Override
    public int weigh(@NonNull Object key, @NonNull Object value) {
        return 1;
    }
}).build();

最大容量策略是所有驱逐策略中最后一个执行的,如果key/value/expire等策略都无法驱逐足够多的缓存kv,最后才会执行按照最大容量策略驱逐。

最大容量策略基于Window Tiny LFU 算法实现,内存友好,命中率高,其理论就不赘述了,网上很多都说的挺好。

每个缓存Node有三种状态(Window、Probation、Protected),表示Node在不同的区域。

BoundedLocalCache#setMaximumSize:

在BoundedLocalCache构造阶段会按照maximumSize确定三个区域的初始大小

其中Window区域较小只有1%,Probation和Protected区域共占99%。

三个区域的大小会在运行时自动调整(climb),来保证更高的缓存命中率。

BoundedLocalCache:每个区域都是一个按照访问顺序排序的双向队列队尾是最新访问的node

Node:包含前后指针。

读写buffer处理

对于缓存的增删改查,Node会根据情况在三个区域中移动,同时也会采样node的访问频率。

AddTask:新增缓存,Node进入Window队尾;

BoundedLocalCache#onAccess:更新/读,Node按照目前状态移动

  1. Window:移动到Window队尾;
  2. Probation:从Probation移除,进入Protected队尾;
  3. Protected:移动到Protected队尾;

RemovalTask:从所属队列移除即可;

驱逐evictEntries

BoundedLocalCache#evictFromWindow:第一步,window溢出需要驱逐node到probation

比如maxSize=1000,windowSize初始是10,由于新增了12个缓存,此时需要window队头的2个node提升到probation队尾。

注:当缓存还未满的时候,只有window溢出这一步。

BoundedLocalCache#evictFromMain:

第二步,如果缓存溢出(超出maximumSize),按照区域+访问频率pk,直到小于最大阈值。

大致逻辑如下:

  1. pk的两个key,分别会从Window(包括刚晋升到Probation的)和Main区(Probation+Protected)选择,Window区的key称为candidateMain区的key称为victim
  2. 每个区域的pk中,candidate 从队尾开始往队头(最近到最远 ),victim 从队头开始往队尾(最远到最近);
  3. 如果candidate输了,candidate被驱逐,candidate指向前面一个node;
  4. 如果victim输了,victim被驱逐,candidate存活,candidate被指向前面一个node,victim被指向后面一个node;
  5. 如果victim所在区域被清理了,candidate也会依次被清理;
  6. 如果candidate所在区域被清理了,victim也会依次被清理;
  7. 如果两个区域都被清理了,victim会按照先Probation后Window的顺序选择区域从头到尾清理;

第一轮pk,candidate=第一步从Window晋升的Probation尾部部分,victim=Probation区域;

接下来会按照实际情况,切换区域pk。比如下面这个例子:

Step1,首先candidate切换为Window队头,因为candidate区域只剩Window了。

Step2,window区域遍历完成,即candidate已经遍历完了,部分key输了,victim区域还未遍历完成,输了一部分。

Step3,将Probation区域从头到尾驱逐,victim切换为Protected区域。

Step4,Protected区域遍历完成,victim切换为剩余Window区域。

BoundedLocalCache#admit频率pk并非单纯的比大小

  1. 如果candidate频率高,则victim被驱逐;
  2. 如果candidate频率低且小于等于5,则candidate被驱逐;
  3. 如果candidate频率低且大于5,按照概率驱逐一方,大概率是驱逐candidate;

频率统计

频率统计通过Count-Min Sketch 算法实现,使用较少空间 ,提供较高准确率

FrequencySketch

  1. SEED:4个种子,简单认为是4种hash算法;
  2. sampleSize:采样阈值,10*maxSize;
  3. table:存储频率;
  4. size:采样次数;
arduino 复制代码
final class FrequencySketch<E> {
    // A mixture of seeds from FNV-1a, CityHash, and Murmur3
    static final long[] SEED = {
        0xc3a5c85c97cb3127L, 
        0xb492b66fbe98f273L, 
        0x9ae16a3b2f90404fL, 
        0xcbf29ce484222325L};

    int sampleSize;
    int tableMask;
    long[] table;
    int size;
}

初始化

FrequencySketch#increment:采样方法有前提条件,必须FrequencySketch初始化后才能采样

BoundedLocalCache.AddTask#run:

只有当缓存到达maximumSize一半之后,FrequencySketch才会初始化。

FrequencySketch#ensureCapacity:

table一旦初始化就不会再扩容了,容量和maximumSize一般一致。

采集频率

采集频率入口有两个:

  1. BoundedLocalCache.AddTask#run:新增缓存;
  2. BoundedLocalCache#onAccess:缓存被访问(更新或读);

FrequencySketch#increment:

  1. 对缓存key做4种hash运算(简单理解为四种),得到四个下标;
  2. 依次对table中4个位置的counter++
  3. 如果采样次数size 超出采样阈值sampleSizereset对所有counter减半;

table是个long数组,每个long拆分为16个counter(4bit),每个counter最大能表示15。

采样数量达到sampleSize(10*max),执行reset ,所有counter统计减半(size当然也减半)。

reset是唯一counter减少的方式,即频率数据不会因为缓存的删除而清理。

查询频率

FrequencySketch#frequency:

  1. hash找到4个counter;
  2. 从4个counter中取最小值返回(CountMin的含义);

窗口调整

BoundedLocalCache#climb:每次maintenance最后,都会调整window的大小,而window的大小也决定了main区域(probation和protected)的大小。

window变大,probation和protected的node会降级到window的尾部;

window变小,window的node会晋升到probation尾部;

BoundedLocalCache#determineAdjustment:窗口调整值的思路是

  1. 采样数量达到10*maxSize才调整
  2. 如果命中率升高,按照步长保持目前的窗口增长或降低的趋势,如果命中率降低,则反向按步长操作;
  3. 按照命中率变化值,持续调整下次的步长

4、过期淘汰

缓存实际过期是异步处理的(maintenance任务)。

在用户查询缓存的时候,会判一下是否过期,保证用户不读到过期值,这个在前面都可以看到,不再赘述。

读写buffer处理

AddTask:新增缓存,三种expire策略进入不同的队列

  1. expireAfterAccess:和最大容量一样,都进入window access队列,按照访问顺序排序;
  2. expireAfterWrite:进入write队列,按照写入顺序排序;
  3. 自定义expire:进入timerWheel时间轮;

UpdateTask:更新缓存,三种策略都是重新放到队尾。

BoundedLocalCache#onAccess:读,除了expireAfterWrite不需要处理,其他都和上面update一样。

RemovalTask:删,从三个地方删除即可。

执行过期expireEntries

BoundedLocalCache#expireEntries:按照access、write、自定义的顺序执行过期。

BoundedLocalCache#expireAfterAccessEntries:expireAfterAccess比较特殊。

除了操作window队列之外,如果同时配置了容量限制,还要操作probation和protected队列,因为window的node会被移动到main区。

BoundedLocalCache#expireAfterAccessEntries:从头(访问时间最远)到尾处理队列。

除了时间判断之外,evictEntry还需要处理一些并发问题。

比如虽然key过期了,但是在驱逐过程中(key级别锁)被其他线程访问导致复活了等等。

BoundedLocalCache#expireAfterWriteEntries:expireAfterWrite只需要处理write队列即可,同理。

BoundedLocalCache#expireVariableEntries:自定义expire时间轮部分不深入分析了。

五、其他

1、线程池

每个Cache支持独立线程池配置,这个线程池一般有四个用途:

  1. refreshAfterWrite:自动刷新;
  2. cache#refresh:手动触发刷新;
  3. RemovalListener:监听缓存被移除;
  4. maintenance任务
less 复制代码
LoadingCache<Object, Object> cache = Caffeine.newBuilder()
            .refreshAfterWrite(Duration.ofHours(1)) // case1
            .maximumSize(10)
            .executor(...) // 设置线程池
            .removalListener(new RemovalListener<Object, Object>(){}) // case3
            .build(new CacheLoader<Object, Object>() {
                @Override
                public @Nullable Object load(@NonNull Object key) throws Exception {
                    return key;
                }
            });

cache.refresh(1); // case2 异步刷新某个key 

如果不配置线程池,默认使用单例ForkJoinPool.commonPool

而这个公用ForkJoinPool常用于其他组件,比如CompleteFuture不指定线程池用这个线程池用于一些非耗时内存操作。如果有耗时操作,比如refreshAfterWrite/refresh通过rpc调用加载数据到本地缓存,还是需要定义自己的executor。

2、缓存移除监听

通过配置removalListenerevictionListener可以感知缓存被移除。

less 复制代码
Cache<Object, Object> cache = Caffeine.newBuilder()
            .maximumSize(1)
            .removalListener(new RemovalListener<Object, Object>() {
                @Override
                public void onRemoval(@Nullable Object key, @Nullable Object value, @NonNull RemovalCause cause) {
                }
            })
            .evictionListener(new RemovalListener<Object, Object>() {
                @Override
                public void onRemoval(@Nullable Object key, @Nullable Object value, @NonNull RemovalCause cause) {
                }
            })
            .build();

两者的主要区别在于,缓存移除的原因RemovalCause不同:

  1. removalListener:支持监听所有移除原因,包括手动操作缓存(EXPLICIT、REPLACED)和自动清理缓存(COLLECTED、EXPIRED、SIZE);
  2. evictionListener:仅监听部分移除原因,如COLLECTED缓存被gc、EXPIRED缓存过期、SIZE由于容量限制被驱逐,见CacheWriterAdapter#delete;

注意RemovalListener收到回调,仅代表缓存entry曾经被移除

BoundedLocalCache#notifyRemoval:

所有回调逻辑,都交给用户配置的executor异步执行(默认是单例ForkJoinPool),不能保证用户按照某个顺序感知缓存过期。

如果executor异常 (比如默认拒绝策略AbortPolicy抛异常),当前线程直接执行listener回调逻辑

3、Metrics

通过recordStats可以开启metrics统计。

less 复制代码
LoadingCache<Object, Object> cache = Caffeine
            .newBuilder()
            .recordStats() // 开启metrics统计
            .build(new CacheLoader<Object, Object>() {
                @Override
                public @Nullable Object load(@NonNull Object key) throws Exception {
                    return key;
                }
            });
        cache.get(1); // missCount++ loadSuccessCount++
        System.out.println(cache.stats());
        cache.get(1); // hitCount++
        System.out.println(cache.stats());

默认实现是ConcurrentStatsCounter,可以通过Caffeine内部埋点回调,统计多个metrics:

  1. hitCount/missCount:缓存命中情况
  2. loadSuccessCount/loadFailureCount/totalLoadTime:mappingFunction/CacheLoader加载缓存情况
  3. evictionCount/evictionWeight:缓存驱逐情况(非手动移除);

4、Policy

Caffeine通过Policy接口 向用户暴露一些运行时底层api

dart 复制代码
LoadingCache<Object, Object> cache = Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(Duration.ofSeconds(30))
            .build(new CacheLoader<Object, Object>(){});
Policy<Object, Object> policy = cache.policy();

过期策略,暴露Policy.Expiration

  1. 运行时调整expire阈值;
  2. 获取topK的young/old缓存;
  3. 获取key的ttl;

BoundedLocalCache#expireAfterWriteOrder:

比如对于expireAfterWrite策略,topk遍历write队列的limit个node。

容量策略,暴露Policy.Eviction

  1. 运行时修改容量;
  2. 获取topK的冷/热缓存;

BoundedLocalCache#evictionOrder:计算topK冷热缓存相对复杂,比如hotest:

  1. protected区域最热,优先取protected区域从尾到头;
  2. protected区域遍历完毕,遍历window和probation区域,从尾到头比较每个node的频率,返回频率大的,如果频率相同,window区域更冷

总结

Cache分类

分类 实现 构造
同步/异步api Cache/AsyncCache build/buildAsync
自动/非自动加载 Cache/LoadingCache build()/build(CacheLoader)
有界/无界 BoundedLocalCache/UnboundedLocalCache refresh、expire、max、key、value/none

有界Cache

BoundedLocalCache有界cache使用率较高:

  1. 是个抽象类,实现类通过javapoet生成;
  2. 使用ConcurrentHashMap存储数据 ,value封装为Node,通过各种compute方法实现key级别锁
  3. Node也是抽象类,实现类通过javapoet生成,根据特性不同具有不同属性;
  4. 用户线程中只会做必要处理(读写cache、记录metrics、记录操作时间),最后将读写task放入mpsc队列;
  5. executor线程处理高级特性,比如有界特性(自动刷新、容量驱逐、过期淘汰等)、RemovalListener、refresh手动刷新;

自动刷新

自动刷新有界特性,处理方式区别于其他高级特性。

自动刷新refreshAfterWrite的实现方式:

  1. 写缓存,在Node上记录writeTime;
  2. 读缓存,判断writeTime过期,触发自动刷新;
  3. cas(node.writeTime, 当前时间+220年) ,确保只有一个线程执行刷新任务;
  4. 提交刷新任务到executor线程;
  5. 执行用户CacheLoader#reload(默认实现是load);
  6. 如果用户代码异常,仅打印日志;
  7. 如果用户代码正常,获取key锁,确认缓存未变更(value和writeTime未改变)的情况下,更新Node;

高级有界特性

其他高级有界特性的处理方式:

  1. 用户线程,增删改查,将读任务放入readbuffer、将写任务放入writebuffer;
  2. executor线程,执行maintenance任务,消费buffer,处理高级特性;
  3. 高级特性处理顺序:弱key->软弱value->过期淘汰->容量驱逐;

容量驱逐

容量驱逐 的实现方式(Window Tiny LFU):

  1. Node按照访问顺序排队;
  2. Node分配在三个区域:window(1%)、probation(99%*20%)、protected(99%*80%),三个区域都有容量限制,按照配置的最大容量分配;
  1. 新Node,采集频率,进入window尾部;
  2. 访问Node,采集频率,node在window则到window尾部,node在probation则晋升到protected,node在protected则到protected尾部;
  1. 如果window容量溢出,window头部晋升到probation;
  2. 如果总容量溢出,将分为candidate(window+溢出window)和victim(probation+protected)两队,按照频率pk;
  3. 频率pk,在频率相同的情况下,如果candidate频率小于等于5,则驱逐candidate;反之按照概率驱逐任意一方,大概率驱逐candidate;
  4. 为了达到更好的缓存命中率,最后还要动态调整window区域的大小(climb);

频率采集 的实现方式(Count-Min Sketch):

  1. 只有当缓存数量到达maximumSize一半之后,才会采集;
  2. 频率数据存储在一个long型数组table中,每个long又被划分为16个4bit的counter,每个counter最多能表示15
  3. 采集频率,对key做4次hash,定位到4个counter都进行计数
  4. 频率衰退 ,采集频率时,如果采集次数size 超出采样阈值sampleSize(10倍maximumSize)reset 对所有counter减半
  5. 查询频率,对key做4次hash,取4个counter中的最小值返回

过期淘汰

处理buffer,将node放入不同队列的尾部:

  1. expireAfterAccess:和容量驱逐一样,进入window access队列,按照访问顺序排序;
  2. expireAfterWrite:进入write队列,按照写入顺序排序;
  3. 自定义expire:进入timerWheel时间轮;

按照access、write、自定义的顺序执行过期。

  1. expireAfterAccess:从头到尾遍历window队列,淘汰过期node,如果开启了容量限制,还要处理probation和protected队列;
  2. expireAfterWrite:从头到尾遍历write queue,淘汰过期node;
  3. 自定义expire:时间轮处理未深入分析;

其他特性

Caffeine需要一组后台线程处理高级特性,默认executor是ForkJoinPool.commonPool,如果有耗时操作,每个cache可配置独立executor。

RemovalListener支持监听缓存被移除:

  1. removalListener:支持所有移除原因,包括手动操作缓存和自动清理缓存;
  2. evictionListener:仅部分移除原因,自动清理缓存;

recordStats支持采集metrics,默认实现是ConcurrentStatsCounter:

  1. hitCount/missCount:缓存命中情况
  2. loadSuccessCount/loadFailureCount/totalLoadTime:mappingFunction/CacheLoader加载缓存情况
  3. evictionCount/evictionWeight:缓存驱逐情况(非手动移除);

Caffeine通过Policy接口 向用户暴露一些运行时底层api

  1. Policy.Expiration:针对expire策略,运行时改变过期时间、获取topK的old/young缓存、获取key的ttl;
  2. Policy.Eviction:针对max策略,运行时修改容量、获取topK的冷/热缓存;
相关推荐
Marst Code3 分钟前
(Django)初步使用
后端·python·django
代码之光_198010 分钟前
SpringBoot校园资料分享平台:设计与实现
java·spring boot·后端
编程老船长23 分钟前
第26章 Java操作Mongodb实现数据持久化
数据库·后端·mongodb
Mephisto.java23 分钟前
【力扣 | SQL题 | 每日四题】力扣2082, 2084, 2072, 2112, 180
sql·算法·leetcode
robin_suli24 分钟前
滑动窗口->dd爱框框
算法
丶Darling.26 分钟前
LeetCode Hot100 | Day1 | 二叉树:二叉树的直径
数据结构·c++·学习·算法·leetcode·二叉树
labuladuo52036 分钟前
Codeforces Round 977 (Div. 2) C2 Adjust The Presentation (Hard Version)(思维,set)
数据结构·c++·算法
IT果果日记44 分钟前
DataX+Crontab实现多任务顺序定时同步
后端
jiyisuifeng19911 小时前
代码随想录训练营第54天|单调栈+双指针
数据结构·算法
科技资讯早知道1 小时前
java计算机毕设课设—坦克大战游戏
java·开发语言·游戏·毕业设计·课程设计·毕设