前言
在当下的系统设计中缓存是相当重要的一环,如果缓存用好了,系统可以的运行速度会有很大的提升,但是如果没用好也可能出现各种各样的问题;在设计缓存过程中我们一般需要考虑到缓存穿透、缓存击穿、缓存雪崩等问题;
缓存穿透
概念
缓存穿透是指在我们访问不存在的数据时,内存将无法获取到这个数据,出于容错考虑我们将从存储层查询该数据,如果存储层也无法查询到该数据,则不写入缓存;这样每次读取数据时都将无法获取到数据,从而失去了缓存的作用,如果请求量大的话,就会导致所有的请求都落到存储层访问;导致存储层访问量多高而出现问题,比如是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对象时的代码 就实现了异步刷新;