本地缓存框架的妙用 -- 合并回源

169 阅读2分钟

正确地使用本地缓存框架,除了能够享受缓存带来的性能提升,还能够享受合并回源带来的流量优化。

思考这么一个场景:数据存储在 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);
            }
        });

优化效果如下: image.png

合并核心代码如下:

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。