guava-cache分析

前言

在当下的系统设计中缓存是相当重要的一环,如果缓存用好了,系统可以的运行速度会有很大的提升,但是如果没用好也可能出现各种各样的问题;在设计缓存过程中我们一般需要考虑到缓存穿透、缓存击穿、缓存雪崩等问题;

缓存穿透

概念

缓存穿透是指在我们访问不存在的数据时,内存将无法获取到这个数据,出于容错考虑我们将从存储层查询该数据,如果存储层也无法查询到该数据,则不写入缓存;这样每次读取数据时都将无法获取到数据,从而失去了缓存的作用,如果请求量大的话,就会导致所有的请求都落到存储层访问;导致存储层访问量多高而出现问题,比如是DB的话,就可能导致DB挂掉;

解决思路

解决缓存穿透问题办法:使用布隆过滤器;即使用一个bitMap存放一个一定不存的数据,如果一定不存在的话,就可以通过bitMap直接过滤掉;另一个办法:如果不存在的话我们直接缓存一个空对象在缓存里;再加个刷新时间;比如1分钟刷新一次,这样到时这个数据有了我们也能获取到; 

guava解决思路:数据数据不存在返回空对象,同时加一个刷新时间;

缓存击穿

概念

对于设置了过期时间的key,在某一段时间可能会成为热点数据,访问量会很高,如果这个时间点刚好过期了,就会访问DB再回写到内存中,但是访问DB是需要一段时间的,而此时如果请求量很高,就会导致请求都落到DB,这就有可能会出现缓存击穿问题;

解决思路

缓存击穿问题解决办法,当key过期而没有获取数据时,第一个请求去DB拿数据,后过来的请求使用锁等待第一个请求返回数据;

guava解决思路:guava当某一个key失效时,如果请求过来时会创建一个Callable线程去DB拿数据同时,如果同时有别的请求过来时,其他请求将使用 Callable 的 get() 等待第一个请求返回;

缓存雪崩

概念

缓存击穿是一个key,雪崩就是有一批的key都同时失效了; 对于同时失效的这个问题我们还可以对key设置不同的失效时间;

guava-cache解决缓存出现的问题

对于穿透问题

private LoadingCache<String, String> cache = CacheBuilder.newBuilder()
        .refreshAfterWrite(1, TimeUnit.SECONDS)
        .maximumSize(200)
        .expireAfterAccess(10, TimeUnit.SECONDS)
        .build(GuavaAsynCacheLoader.asynLoader(new CacheLoader<String, String>() {

            @Override
            public String load(String s) throws Exception {

                Thread.sleep(5000);
                System.out.println("数据刷新==========");
                //访问DB 如果出现空返回一个空对象 
                   
                return "";
            }
        }));

当访问到一个不存在的数据时,load返回空对象;再加上refreshAfterWrite 定时刷新 就能很好的解决缓存穿透问题了;

expireAfterAccess 配置失效时间,不同的key放在不同的cache中 失效时间不要一致能解决缓存雪崩问题;

对于缓存击穿问题 请看源码

V lockedGetOrLoad(K key, int hash, CacheLoader<? super K, V> loader)
    throws ExecutionException {
  ReferenceEntry<K, V> e;
  ValueReference<K, V> valueReference = null;
  LoadingValueReference<K, V> loadingValueReference = null;
  boolean createNewEntry = true;

  lock();
  try {
    // re-read ticker once inside the lock
    long now = map.ticker.read();
    preWriteCleanup(now);

    int newCount = this.count - 1;
    AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
    int index = hash & (table.length() - 1);
    ReferenceEntry<K, V> first = table.get(index);

    for (e = first; e != null; e = e.getNext()) {
      K entryKey = e.getKey();
      if (e.getHash() == hash && entryKey != null
          && map.keyEquivalence.equivalent(key, entryKey)) {
        valueReference = e.getValueReference();
        if (valueReference.isLoading()) {
          createNewEntry = false;
        } else {
          V value = valueReference.get();
          if (value == null) {
            enqueueNotification(entryKey, hash, valueReference, RemovalCause.COLLECTED);
          } else if (map.isExpired(e, now)) {
            // This is a duplicate check, as preWriteCleanup already purged expired
            // entries, but let's accomodate an incorrect expiration queue.
            enqueueNotification(entryKey, hash, valueReference, RemovalCause.EXPIRED);
          } else {
            recordLockedRead(e, now);
            statsCounter.recordHits(1);
            // we were concurrent with loading; don't consider refresh
            return value;
          }

          // immediately reuse invalid entries
          writeQueue.remove(e);
          accessQueue.remove(e);
          this.count = newCount; // write-volatile
        }
        break;
      }
    }

    if (createNewEntry) {
     //第一个请求过来时  ,key -value 不存在值时,会创建一个 loadingValueReference 对象  
     //这个对象中有个 futureValue对象即Callable 线程
      loadingValueReference = new LoadingValueReference<K, V>();

      if (e == null) {
        e = newEntry(key, hash, first);
        e.setValueReference(loadingValueReference);
        table.set(index, e);
      } else {
        e.setValueReference(loadingValueReference);
      }
    }
  } finally {
    unlock();
    postWriteCleanup();
  }

  if (createNewEntry) {
    try {
      // Synchronizes on the entry to allow failing fast when a recursive load is
      // detected. This may be circumvented when an entry is copied, but will fail fast most
      // of the time.
      synchronized (e) {
        return loadSync(key, hash, loadingValueReference, loader);
      }
    } finally {
      statsCounter.recordMisses(1);
    }
  } else {
    // The entry already exists. Wait for loading.
    // 之后的请求
    return waitForLoadingValue(e, key, valueReference);
  }
}

从这段代码中可以发现已经很好的解决了缓存击穿问题;不会出现大量的请求通知访问DB了;

在guava中对于缓存到了刷新时间,默认情况下是同步刷新的,但是也是第一次请求刷新的请求会等会最新刷新结果,当在刷新时其他过来的请求会返回oldValue ;

V scheduleRefresh(ReferenceEntry<K, V> entry, K key, int hash, V oldValue, long now,
    CacheLoader<? super K, V> loader) {
  if (map.refreshes() && (now - entry.getWriteTime() > map.refreshNanos)
      && !entry.getValueReference().isLoading()) {
    V newValue = refresh(key, hash, loader, true);
    if (newValue != null) {
      return newValue;
    }
  }
  // newValue 说明不是第一个请求 直接先返回老值
  return oldValue;
}

 

@Nullable
// 第一个请求返回loadingValueReference对象  
LoadingValueReference<K, V> insertLoadingValueReference(final K key, final int hash,
    boolean checkTime) {
  ReferenceEntry<K, V> e = null;
  lock();
  try {
    long now = map.ticker.read();
    preWriteCleanup(now);

    AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
    int index = hash & (table.length() - 1);
    ReferenceEntry<K, V> first = table.get(index);

    // Look for an existing entry.
    for (e = first; e != null; e = e.getNext()) {
      K entryKey = e.getKey();
      if (e.getHash() == hash && entryKey != null
          && map.keyEquivalence.equivalent(key, entryKey)) {
        // We found an existing entry.

        ValueReference<K, V> valueReference = e.getValueReference();
       // 如果是第二个请求 valueReference 是 loadingVaueReference 将返回空
        if (valueReference.isLoading()
            || (checkTime && (now - e.getWriteTime() < map.refreshNanos))) {
          // refresh is a no-op if loading is pending
          // if checkTime, we want to check *after* acquiring the lock if refresh still needs
          // to be scheduled
          return null;
        }

        // continue returning old value while loading
        ++modCount;
        LoadingValueReference<K, V> loadingValueReference =
            new LoadingValueReference<K, V>(valueReference);
       //  设置valueReference 为  loadingValueReference
        e.setValueReference(loadingValueReference);
        return loadingValueReference;
      }
    }

    ++modCount;
    LoadingValueReference<K, V> loadingValueReference = new LoadingValueReference<K, V>();
    e = newEntry(key, hash, first);
    e.setValueReference(loadingValueReference);
    table.set(index, e);
    return loadingValueReference;
  } finally {
    unlock();
    postWriteCleanup();
  }
}

结合上面的两段代码可以得到结论:只有第一次请求会执行刷新操作,从第二个请求开始就不再刷新,直接返回oldValue;

那如何实现异步刷新呢?请看下面这段代码

在创建CacheLoader时 我们需要重写 reload方法, CacheLoader 已经提供了一个异步刷新实现reload的方法了,我们只要使用就可以;

public static class GuavaAsynCacheLoader{

    private static Executor executor = Executors.newFixedThreadPool(5);
    public static CacheLoader asynLoader(CacheLoader cacheLoader){
        return CacheLoader.asyncReloading(cacheLoader,executor);
    }
}

结合创建cache对象时的代码 就实现了异步刷新;

转载于:https://my.oschina.net/u/1414315/blog/1823150

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值