LoadingCache刷新缓存策略

6,546 阅读2分钟

记录工作中使用到的异步刷新缓存方法。

Guava cache是Java项目中很常用的本地缓存。CacheLoader是Guava cache常用的缓存加载方式。

1、定时过期

LoadingCache<String, Object> cache = CacheBuilder.newBuilder()
      .maximumSize(100)       
      .expireAfterWrite(1, TimeUnit.SECONDS)     //根据写入时间过期
      .build(new CacheLoader<String, Object>() {
        @Override public Object load(String key) {
          return generateValueByKey(key);
        }
      });

按照expireAfterWrite方式来让已写入的缓存过期。这种方式存在一个问题:当高并发同时get同一个缓存值,而此时该缓存过期,就会有一个线程进入load方法,而其他线程则阻塞等待,直到缓存值被生成。虽然这样避免了缓存击穿的危险,但还是有大量线程阻塞等待生成缓存值。

guava源码:

V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
            try {
                if (this.count != 0) {
                    LocalCache.ReferenceEntry<K, V> e = this.getEntry(key, hash);
                    if (e != null) {
                        long now = this.map.ticker.read();
                        V value = this.getLiveValue(e, now);
                        if (value != null) {
                            this.recordRead(e, now);
                            this.statsCounter.recordHits(1);
                            // 缓存值存在,判断是否需要刷新,并获取新的缓存值
                            Object var17 = this.scheduleRefresh(e, key, hash, value, now, loader);
                            return var17;
                        }

                        LocalCache.ValueReference<K, V> valueReference = e.getValueReference();
                        if (valueReference.isLoading()) {    
                            //发现该缓存值正在被加载,则等待直到缓存值被生成
                            Object var9 = this.waitForLoadingValue(e, key, valueReference);
                            return var9;
                        }
                    }
                }

                Object var15 = this.lockedGetOrLoad(key, hash, loader);   
                //第一次get,会进去lockedGetOrLoad方法去加载缓存值
                return var15;
            } catch(Exception e){...}

2、定时刷新

LoadingCache<String, Object> cache = CacheBuilder.newBuilder()
      .maximumSize(100)       
      .refreshAfterWrite(1, TimeUnit.SECONDS)     //根据写入时间去刷新缓存值
      .build(new CacheLoader<String, Object>() {
        @Override public Object load(String key) {
          return generateValueByKey(key);
        }
      });

当到达刷新时间是,会有一个用户线程去刷新缓存值,其他线程仍获取旧值。虽然这样每个key只会阻塞一个用户线程,但高并发请求不同key时,仍然会造成大量线程阻塞,并对数据库压力过大。

guava源码:

V scheduleRefresh(LocalCache.ReferenceEntry<K, V> entry, K key, int hash, V oldValue, long now, CacheLoader<? super K, V> loader) {
            if (this.map.refreshes() && now - entry.getWriteTime() > this.map.refreshNanos) {
                V newValue = this.refresh(key, hash, loader);
                if (newValue != null) {
                    return newValue;
                }
            }

            return oldValue;   //当newValue为null,就返回oldValue
        }
V refresh(K key, int hash, CacheLoader<? super K, V> loader) {
            LocalCache.LoadingValueReference<K, V> loadingValueReference = this.insertLoadingValueReference(key, hash);
            if (loadingValueReference == null) {
                return null;   //若正在加载该缓存值,就返回null
            } else {
                //重新加载缓存值
                ListenableFuture<V> result = this.loadAsync(key, hash, loadingValueReference, loader);
                // 默认result是调用load方法,并将其结果构造成ListenableFuture返回,所以是同步加载调用load
                if (result.isDone()) {
                    try {
                        return result.get();
                    } catch (Throwable var7) {
                    }
                }

                return null;
            }
        }

3、异步刷新

ExecutorService executorService = Executors.newFixedThreadPool(10);
    LoadingCache<String, Object> cache = CacheBuilder.newBuilder().maximumSize(100)
      .refreshAfterWrite(1, TimeUnit.SECONDS)    
      .build(new CacheLoader<String, Object>() {
        @Override public Object load(String key) {
          return generateValueByKey(key);
        }

        @Override public ListenableFuture<Object> reload(String key, Object oldValue) {
          ListenableFutureTask<Object> task =
            new ListenableFutureTask<Object>(new Callable<Object>() {
              @Override public Object call() {
                return generateValueByKey(key);
              }
            });
          executorService.submit(task);
          return task;
        }
      });

上面代码中是使用executorService线程池去异步的刷新缓存值。