前言
本章分析caffeine2.9.3版本的实现。
- 梳理通过不同api构造caffeine cache的分类;
- 有界特性的实现方式,如自动刷新 、过期淘汰 、容量驱逐;
- 其他边缘特性,如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:
- Cache ,Caffeine#build 构造返回,同步Cache实现,用户通过cache.get直接得到value;
- AsyncCache ,Caffeine#buildAsync 构造返回,异步AsyncCache实现,用户通过cache.get得到value的future;
如下面的类图显示,同步cache和异步cache是两套独立的接口和实现:
细节上,是否自动加载key对应value到cache又分为两种:
- 普通Cache ,用户build不传入CacheLoader ,cache miss仅支持通过get(key,mappingFunction)加载value或手动put;
- LoadingCache ,用户build传入CacheLoader ,cache miss支持get(key)自动通过CacheLoader加载kv到cache;
我们以同步cache为例,进行后续的分析。
二、Cache骨架
1、Cache
Cache
Cache是暴露给用户的顶层api:
- 线程安全;
- 需要主动调用get/put方法存储数据;
- 支持手动清理;
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的骨架实现:
- 继承Cache骨架LocalManualCache ,能获取cache方法返回的LocalCache,操作底层cache;
- 依赖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
- 查询老缓存oldValue;
- 根据oldValue是否存在,调用CacheLoader的load或reload方法,异步加载newValue;这一步发生异常,不更新缓存;
- 比较当前缓存currentValue和oldValue和newValue的关系,决定最终存储哪个value;
- 如果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接口定义了两个方法:
- load:加载key对应value,允许返回空,代表value不存在,允许抛出异常,保持底层缓存不变;
- 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实现有两种:
- BoundedLocalManualCache:手动Cache;
- 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;
}
DrainStatusRef 是BoundedLocalCache父类,维护了一个状态drainStatus。
scala
abstract static class DrainStatusRef<K, V> extends PadDrainStatus<K, V> {
/** The draining status of the buffers. */
volatile int drainStatus = IDLE;
}
PadDrainStatus 是DrainStatusRef 的父类,解决伪共享问题,帮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)情况有关:
- 容量限制 :W-TinyLFU,需要实现AccessOrder让Node按照访问顺序形成链表;
- 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)情况有关:
- refreshAfterWrite:需要记录写入时间writeTime;
- 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框架通过编码方式生成的:
- BoundedLocalCache抽象类的实现 ,通过LocalCacheFactory工厂获取;
- 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:忽略特性,有三种情况
- key对应node不存在,直接返回null;
- key对应node存在,但是过期(expire)或value被gc(soft/weakValues),返回null;
- 正常返回缓存值;
可以看到,查询时会涉及缓存过期判断。即过期缓存即使还未清理,也不会返回给用户。
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:
- fast-path,如果data中存在未过期node或未gc的value,直接返回缓存,这是一次无锁普通读(ConcurrentHashMap无锁);
- 其他情况,即没有合适的缓存,需要走有锁逻辑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对应缓存不存在/过期的情况,可能需要写
- S1,利用ConcurrentHashMap的compute方法,获取key级别锁
- S2,如果当前缓存Node不存在,执行用户查询逻辑(mappingFunction/CacheLoader)
-
- 查询返回为空,什么都不做;
- 查询返回非空,封装为Node放入data;
- S3,如果当前缓存Node存在 ,获取Node级别锁
-
- 如果node未过期,value未gc,直接返回这个node,这是一次普通读;
- 否则执行用户查询逻辑;
- 如果用户查询结果为null,移除缓存Node;如果非空,更新缓存Node;
- 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上锁 ,在这中间穿插了许多有界缓存特性处理。
四、有界特性
有界特性大致分为四类:
- 最大容量策略(maximum):基于总容量size(权重是1的特例)、基于权重weight;
- 过期策略(expire):写后过期write、读写后过期access、自定义Expiry过期;
- key/value策略:软value、弱key、弱value;
- 自动刷新策略:使用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正常:
- 如果reload期间,value未发生变化,设置为reload返回value;
- 反之,保持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都是mpsc (multiple producer single consumer)的一种实现,无锁都是cas。
ReadBuffer
readBuffer的实现是BoundedBuffer,不保证有序(因为有个hash):
- RingBuffer:底层环形数组,容量16,有填充(ReadAndWriteCounterRef)防止伪共享问题;
- 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,大致特点:
- 多个2^n+1容量数组组成,每个数组的最后一个元素,存储下一个数组的开始位置;
- 前2^n-1个位置存储实际元素,2^n位置存储标志JUMP;
- 生产者多线程,操作生产下标,需要cas,如果超出容量,需要扩容,按照当前数组的两倍扩容;
- 消费者单线程,操作消费下标,不需要cas,但是要保证可见性(A线程和B线程不同时消费,但是A消费完可以切换B消费),如果遇到JUMP跳转到下一个数组;
- 在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:有界特性的核心方法。
- drainReadBuffer:处理读buffer;
- drainWriteBuffer:处理写buffer;
- drainKeyReferences:处理弱key,忽略;
- drainValueReferences:处理软value/弱value,忽略;
- expireEntries:处理过期;
- evictEntries:处理最大容量驱逐;
- 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按照目前状态移动
- Window:移动到Window队尾;
- Probation:从Probation移除,进入Protected队尾;
- Protected:移动到Protected队尾;
RemovalTask:从所属队列移除即可;
驱逐evictEntries
BoundedLocalCache#evictFromWindow:第一步,window溢出需要驱逐node到probation。
比如maxSize=1000,windowSize初始是10,由于新增了12个缓存,此时需要window队头的2个node提升到probation队尾。
注:当缓存还未满的时候,只有window溢出这一步。
BoundedLocalCache#evictFromMain:
第二步,如果缓存溢出(超出maximumSize),按照区域+访问频率pk,直到小于最大阈值。
大致逻辑如下:
- pk的两个key,分别会从Window(包括刚晋升到Probation的)和Main区(Probation+Protected)选择,Window区的key称为candidate ,Main区的key称为victim;
- 每个区域的pk中,candidate 从队尾开始往队头(最近到最远 ),victim 从队头开始往队尾(最远到最近);
- 如果candidate输了,candidate被驱逐,candidate指向前面一个node;
- 如果victim输了,victim被驱逐,candidate存活,candidate被指向前面一个node,victim被指向后面一个node;
- 如果victim所在区域被清理了,candidate也会依次被清理;
- 如果candidate所在区域被清理了,victim也会依次被清理;
- 如果两个区域都被清理了,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并非单纯的比大小
- 如果candidate频率高,则victim被驱逐;
- 如果candidate频率低且小于等于5,则candidate被驱逐;
- 如果candidate频率低且大于5,按照概率驱逐一方,大概率是驱逐candidate;
频率统计
频率统计通过Count-Min Sketch 算法实现,使用较少空间 ,提供较高准确率。
FrequencySketch:
- SEED:4个种子,简单认为是4种hash算法;
- sampleSize:采样阈值,10*maxSize;
- table:存储频率;
- 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一般一致。
采集频率
采集频率入口有两个:
- BoundedLocalCache.AddTask#run:新增缓存;
- BoundedLocalCache#onAccess:缓存被访问(更新或读);
FrequencySketch#increment:
- 对缓存key做4种hash运算(简单理解为四种),得到四个下标;
- 依次对table中4个位置的counter++ ;
- 如果采样次数size 超出采样阈值sampleSize ,reset对所有counter减半;
table是个long数组,每个long拆分为16个counter(4bit),每个counter最大能表示15。
采样数量达到sampleSize(10*max),执行reset ,所有counter统计减半(size当然也减半)。
reset是唯一counter减少的方式,即频率数据不会因为缓存的删除而清理。
查询频率
FrequencySketch#frequency:
- hash找到4个counter;
- 从4个counter中取最小值返回(CountMin的含义);
窗口调整
BoundedLocalCache#climb:每次maintenance最后,都会调整window的大小,而window的大小也决定了main区域(probation和protected)的大小。
window变大,probation和protected的node会降级到window的尾部;
window变小,window的node会晋升到probation尾部;
BoundedLocalCache#determineAdjustment:窗口调整值的思路是
- 采样数量达到10*maxSize才调整;
- 如果命中率升高,按照步长保持目前的窗口增长或降低的趋势,如果命中率降低,则反向按步长操作;
- 按照命中率变化值,持续调整下次的步长;
4、过期淘汰
缓存实际过期是异步处理的(maintenance任务)。
在用户查询缓存的时候,会判一下是否过期,保证用户不读到过期值,这个在前面都可以看到,不再赘述。
读写buffer处理
AddTask:新增缓存,三种expire策略进入不同的队列
- expireAfterAccess:和最大容量一样,都进入window access队列,按照访问顺序排序;
- expireAfterWrite:进入write队列,按照写入顺序排序;
- 自定义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支持独立线程池配置,这个线程池一般有四个用途:
- refreshAfterWrite:自动刷新;
- cache#refresh:手动触发刷新;
- RemovalListener:监听缓存被移除;
- 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、缓存移除监听
通过配置removalListener 或evictionListener可以感知缓存被移除。
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不同:
- removalListener:支持监听所有移除原因,包括手动操作缓存(EXPLICIT、REPLACED)和自动清理缓存(COLLECTED、EXPIRED、SIZE);
- 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:
- hitCount/missCount:缓存命中情况;
- loadSuccessCount/loadFailureCount/totalLoadTime:mappingFunction/CacheLoader加载缓存情况;
- 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:
- 运行时调整expire阈值;
- 获取topK的young/old缓存;
- 获取key的ttl;
BoundedLocalCache#expireAfterWriteOrder:
比如对于expireAfterWrite策略,topk遍历write队列的limit个node。
容量策略,暴露Policy.Eviction:
- 运行时修改容量;
- 获取topK的冷/热缓存;
BoundedLocalCache#evictionOrder:计算topK冷热缓存相对复杂,比如hotest:
- protected区域最热,优先取protected区域从尾到头;
- 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使用率较高:
- 是个抽象类,实现类通过javapoet生成;
- 使用ConcurrentHashMap存储数据 ,value封装为Node,通过各种compute方法实现key级别锁;
- Node也是抽象类,实现类通过javapoet生成,根据特性不同具有不同属性;
- 用户线程中只会做必要处理(读写cache、记录metrics、记录操作时间),最后将读写task放入mpsc队列;
- executor线程处理高级特性,比如有界特性(自动刷新、容量驱逐、过期淘汰等)、RemovalListener、refresh手动刷新;
自动刷新
自动刷新有界特性,处理方式区别于其他高级特性。
自动刷新refreshAfterWrite的实现方式:
- 写缓存,在Node上记录writeTime;
- 读缓存,判断writeTime过期,触发自动刷新;
- cas(node.writeTime, 当前时间+220年) ,确保只有一个线程执行刷新任务;
- 提交刷新任务到executor线程;
- 执行用户CacheLoader#reload(默认实现是load);
- 如果用户代码异常,仅打印日志;
- 如果用户代码正常,获取key锁,确认缓存未变更(value和writeTime未改变)的情况下,更新Node;
高级有界特性
其他高级有界特性的处理方式:
- 用户线程,增删改查,将读任务放入readbuffer、将写任务放入writebuffer;
- executor线程,执行maintenance任务,消费buffer,处理高级特性;
- 高级特性处理顺序:弱key->软弱value->过期淘汰->容量驱逐;
容量驱逐
容量驱逐 的实现方式(Window Tiny LFU):
- Node按照访问顺序排队;
- Node分配在三个区域:window(1%)、probation(99%*20%)、protected(99%*80%),三个区域都有容量限制,按照配置的最大容量分配;
- 新Node,采集频率,进入window尾部;
- 访问Node,采集频率,node在window则到window尾部,node在probation则晋升到protected,node在protected则到protected尾部;
- 如果window容量溢出,window头部晋升到probation;
- 如果总容量溢出,将分为candidate(window+溢出window)和victim(probation+protected)两队,按照频率pk;
- 频率pk,在频率相同的情况下,如果candidate频率小于等于5,则驱逐candidate;反之按照概率驱逐任意一方,大概率驱逐candidate;
- 为了达到更好的缓存命中率,最后还要动态调整window区域的大小(climb);
频率采集 的实现方式(Count-Min Sketch):
- 只有当缓存数量到达maximumSize一半之后,才会采集;
- 频率数据存储在一个long型数组table中,每个long又被划分为16个4bit的counter,每个counter最多能表示15;
- 采集频率,对key做4次hash,定位到4个counter都进行计数;
- 频率衰退 ,采集频率时,如果采集次数size 超出采样阈值sampleSize(10倍maximumSize) ,reset 对所有counter减半;
- 查询频率,对key做4次hash,取4个counter中的最小值返回;
过期淘汰
处理buffer,将node放入不同队列的尾部:
- expireAfterAccess:和容量驱逐一样,进入window access队列,按照访问顺序排序;
- expireAfterWrite:进入write队列,按照写入顺序排序;
- 自定义expire:进入timerWheel时间轮;
按照access、write、自定义的顺序执行过期。
- expireAfterAccess:从头到尾遍历window队列,淘汰过期node,如果开启了容量限制,还要处理probation和protected队列;
- expireAfterWrite:从头到尾遍历write queue,淘汰过期node;
- 自定义expire:时间轮处理未深入分析;
其他特性
Caffeine需要一组后台线程处理高级特性,默认executor是ForkJoinPool.commonPool,如果有耗时操作,每个cache可配置独立executor。
RemovalListener支持监听缓存被移除:
- removalListener:支持所有移除原因,包括手动操作缓存和自动清理缓存;
- evictionListener:仅部分移除原因,自动清理缓存;
recordStats支持采集metrics,默认实现是ConcurrentStatsCounter:
- hitCount/missCount:缓存命中情况;
- loadSuccessCount/loadFailureCount/totalLoadTime:mappingFunction/CacheLoader加载缓存情况;
- evictionCount/evictionWeight:缓存驱逐情况(非手动移除);
Caffeine通过Policy接口 向用户暴露一些运行时底层api:
- Policy.Expiration:针对expire策略,运行时改变过期时间、获取topK的old/young缓存、获取key的ttl;
- Policy.Eviction:针对max策略,运行时修改容量、获取topK的冷/热缓存;