正确地使用本地缓存框架,除了能够享受缓存带来的性能提升,还能够享受合并回源带来的流量优化。
思考这么一个场景:数据存储在 DB 中,同样的请求得到相同的数据,在实例层面就可以同一 ( 段 ) 时间内的请求合并成一个请求落到 DB,如此简单优化就可以轻松地将流量压力丢给无状态的实例扛,对 DB 的压力也就变成跟请求数无关,跟实例数、时间间隔相关,实例数和时间间隔相对固定且量小。
接着来看如何正确得使用本地缓存框架 ( 以 Google Guava Cache 为例 ):
// 实现字符串共享池
private final LoadingCache<String, String> keyPool = CacheBuilder.newBuilder()
.recordStats()
.softValues().maximumSize(100000)
.concurrencyLevel(Runtime.getRuntime().availableProcessors())
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) {
return key;
}
});
keyPool.get(key);
或者
// 实现字符串共享池
private final Cache<String, String> keyPool = CacheBuilder.newBuilder()
.recordStats()
.softValues().maximumSize(100000)
.concurrencyLevel(Runtime.getRuntime().availableProcessors())
.build();
keyPool.get(key, () -> key);
以上两种用法就可以享受到合并回源,而如下的写法则会在 cache missed 无法享受合并回源:
private List<PromotionRoot> doGetPromotionList(List<String> cacheKeys){
if (CollectionUtils.isEmpty(cacheKeys)) return Collections.emptyList();
// 获取本地已存在的
Map<String, PromotionRoot> existed = cacheKeys.stream()
.collect(Collectors.toMap(Function.identity(), key -> Optional.ofNullable(valuePool.getIfPresent(key))))
.entrySet().stream().filter(e -> e.getValue().isPresent())
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get()));
String[] missedKey = cacheKeys.stream().filter(e -> !existed.containsKey(e)).toArray(String[]::new);
if (missedKey.length == 0) return existed.values().stream().collect(Collectors.toList());
// 查询本地不存在的
Map<String, PromotionRoot> missed = Optional.ofNullable(redisAssistant.mGet(missedKey)).orElse(Collections.emptyList())
.stream().filter(Objects::nonNull)
.map(e -> JSON.parseObject(e, PromotionRoot.class))
.collect(Collectors.toMap(e -> getCacheKey(e.getPromotionId().getPromotionId()), Function.identity()));
// 更新本地不存在的
missed.forEach(valuePool::put);
// 合并已存在和刚获取的
return Stream.concat(existed.values().stream(), missed.values().stream()).collect(Collectors.toList());
}
private final com.google.common.cache.Cache<String, PromotionRoot> valuePool = CacheBuilder.newBuilder()
.recordStats()
.concurrencyLevel(Runtime.getRuntime().availableProcessors())
.softValues().maximumSize(1000).expireAfterWrite(10, TimeUnit.SECONDS)
.build();
上面这种写法可以考虑在外层增加一个 list 级别的合并回源:
public List<PromotionRoot> getPromotionList(List<Long> promotionIds) {
if (CollectionUtils.isEmpty(promotionIds)) return Collections.emptyList();
List<String> cacheKeys = promotionIds.stream()
.distinct().sorted().map(this::getCacheKey).collect(Collectors.toList());
return listPool.get(cacheKeys);
}
private final LoadingCache<List<String>, List<PromotionRoot>> listPool = CacheBuilder.newBuilder()
.recordStats()
.concurrencyLevel(Runtime.getRuntime().availableProcessors())
.softValues().maximumSize(1000).expireAfterWrite(10, TimeUnit.SECONDS)
.build(new CacheLoader<List<String>, List<PromotionRoot>>() {
@Override
public List<PromotionRoot> load(List<String> cacheKeys) {
return doGetPromotionList(cacheKeys);
}
});
优化效果如下:
合并核心代码如下:
V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
checkNotNull(key);
checkNotNull(loader);
try {
if (count != 0) { // read-volatile
// don't call getLiveEntry, which would ignore loading values
ReferenceEntry<K, V> e = getEntry(key, hash);
if (e != null) {
long now = map.ticker.read();
V value = getLiveValue(e, now);
if (value != null) {
recordRead(e, now);
statsCounter.recordHits(1);
return scheduleRefresh(e, key, hash, value, now, loader);
}
ValueReference<K, V> valueReference = e.getValueReference();
// 当前请求在加载中,则等待加载结果
if (valueReference.isLoading()) {
return waitForLoadingValue(e, key, valueReference);
}
}
}
// at this point e is either null or expired;
return lockedGetOrLoad(key, hash, loader);
} catch (ExecutionException ee) {
Throwable cause = ee.getCause();
if (cause instanceof Error) {
throw new ExecutionError((Error) cause);
} else if (cause instanceof RuntimeException) {
throw new UncheckedExecutionException(cause);
}
throw ee;
} finally {
postReadCleanup();
}
}
此外,在使用本地缓存的时候,除了设置最大缓存数量,还要适当设置 softValues() / weakValues() 以避免大 value 场景下的 OOM,在使用 weakKeys() 的时候则需要自定义 equals。