首先,咱们都有一共识,即可以使用缓存来提升系统的访问速度!
现如今,分布式缓存这么强大,所以,大部分时候,我们可能都不会去关注本地缓存了!
而在一起高并发的场景,如果我们一味使用nosql式的缓存,如 redis, 那么也是好的吧!
但是有个问题我们得考虑下: redis 这样的缓存是快,但是它总有自己的瓶颈吧,如果什么东西我们都往里面存储,则在高并发场景下,应用瓶颈将受限于其缓存瓶颈吧!
所以,针对这种问题,在一些场景下,咱们可以使用本地缓存来存储一些数据,从而避免每次都将请求击穿到 redis 层面!
本文考虑的是使用 本地缓存 作为二级缓存存在,而非直接的充当缓存工具!
而使用本地缓存,则有几个讲究:
1. 缓存一致性问题;
2. 并发安全问题;所谓缓存一致性问题,就是本地缓存,是否和redis等缓存中间件的数据保持一致,如果不一致的表现超过了可接受的程度,则要这访问速度也就没啥意义了!
所谓并发安全问题,即是,当使用本地缓存时,本地的缓存访问线程安全性问题,如果出现错乱情况,则严重了!
使用本地缓存,有什么好处?
1. 减少访问远程缓存的网络io,速度自然是要提升的;
2. 减少远程缓存的并发请求,从而表现出更大的并发处理能力;本地缓存,都有什么应用场景?
1. 单机部署的应用咱们就不说了;
2. 读多写少的场景;(这也缓存的应用场景) 3. 可以容忍一定时间内的缓存不一致; (因涉及的本地缓存,分布式机器结果必可能不一致) 4. 应用对缓存的请求量非常大的场景; (如果直接打到redis缓存, 则redis压力巨大, 且应用响应速度将变慢)所以,如果自己存在这样的使用场景,不防也考虑下,如何使用这本地缓存,来提升响应速度吧!
如果要求自己来实现这两级缓存功能,我想应该也是不能的!只要解决掉两个问题即可:
1. 缓存过期策略;
2. 缓存安全性;其中一最简单直接的方式,就是使用一个定时刷新缓存的线程,在时间节点到达后,将缓存删除即可; 另一个安全问题,则可以使用 synchronized 或 并发包下的锁工具来实现即可。
但是真正做起来,可能不一定会简单,这也不是咱们想特别考虑的。
咱们主要看下 guava 是如何来解决这种问题的?以其思路来开拓自己的设想!
如何 guava 来作为我们的二级缓存?
1. 首先,咱们得引入 guava 的依赖
com.google.guava guava 18.0
2. 创建guava缓存;
首先,创建的guava缓存实例,应是全局共用的,否则就失去了缓存的意义;
其次,一些过期参数,应支持配置化;
例子如下:
@Component@Slf4jpublic class LocalEnhancedCacheHolder { @Value("${guava.cache.max.size}") private Integer maxCacheSize; @Value("${guava.cache.timeout}") private Integer guavaCacheTimeout; /** * 字符串类型取值, k->v 只支持字符串存取,避免复杂化 */ private LoadingCachestringDbCacheContainer; /** * hash 数据缓存, 展示使用多key 作为 guava 的缓存key */ private LoadingCache hashDbCacheContainer; @Resource private RedisTemplate redisTemplate; /** * 值为空 字符串标识 */ public static final String EMPTY_VALUE_STRING = ""; /** * 值为空 字节标识 */ public static final byte[] EMPTY_VALUE_BYTES = new byte[0]; @PostConstruct public void init() { Integer dbCount = 2; stringDbCacheContainer = CacheBuilder.newBuilder() .expireAfterWrite(guavaCacheTimeout, TimeUnit.SECONDS) .maximumSize(maxCacheSize) .build(new CacheLoader () { @Override public String load(String key) throws Exception { log.info("【缓存】从redis获取配置:{}", key); String value = redisTemplate.get(key); return StringUtils.defaultIfBlank(value, EMPTY_VALUE_STRING); } }); hashDbCacheContainer = CacheBuilder.newBuilder() .expireAfterWrite(guavaCacheTimeout, TimeUnit.SECONDS) .maximumSize(maxCacheSize / dbCount) .build(new CacheLoader () { @Override public byte[] load(HashDbItemEntry keyHolder) throws Exception { log.info("【缓存】从redis获取配置:{}", keyHolder); byte[] valueBytes = redisTemplate.hgetValue( keyHolder.getBucketKey(), keyHolder.getSlotKey()); if(valueBytes == null) { valueBytes = EMPTY_VALUE_BYTES; } return valueBytes; } }); } /** * 获取k-v中的缓存值 * * @param key 键 * @return 缓存值,没有值时,返回 null */ public String getCache(String key) { try { return stringDbCacheContainer.get(key); } catch (ExecutionException e) { log.error("【缓存】获取缓存异常:{}, ex:{}", key, e); throw new RuntimeException(e); } } /** * 放入缓存,此处暂只实现为向redis写入值 * * @param key 缓存key * @param value 缓存value */ public void putCache(String key, String value) { redisTemplate.set(key, value, 0L); } /** * 放入缓存带超时时间设置,此处暂只实现为向redis写入值 * * @param key 缓存key * @param value 缓存value * @param timeout 超时时间,单位 s */ public void putCache(String key, String value, Long timeout) { redisTemplate.set(key, value, timeout); } /** * 删除单个kv缓存 * * @param key 缓存键 */ public void removeCache(String key) { redisTemplate.remove(key); } /** * 批量删除单个kv缓存 * * @param keyList 缓存键 列表,以 管道形式删除,性能更高 */ public void removeCache(Collection keyList) { redisTemplate.remove(keyList); } /** * 从hash数据库中获取缓存值 * * @param bucketKey 桶key, 对应一系列值 k->v * @param slotKey 槽key, 对应具体的缓存值 * @return 缓存值 */ public byte[] getCacheFromHash(String bucketKey, String slotKey) { HashDbItemEntry entry = new HashDbItemEntry(bucketKey, slotKey); try { return hashDbCacheContainer.get(entry); } catch (ExecutionException e) { log.error("【缓存】获取缓存异常:{}, ex:{}", entry, e); throw new RuntimeException(e); } } /** * hash 数据结构存储 * * value 暂不存储相应值,只做查询使用 */ class HashDbItemEntry { private String bucketKey; private String slotKey; private Object value; public HashDbItemEntry(String bucketKey, String slotKey) { this.bucketKey = bucketKey; this.slotKey = slotKey; } public String getBucketKey() { return bucketKey; } public String getSlotKey() { return slotKey; } public Object getValue() { return value; } // 必重写 equals & hashCode, 否则缓存将无法复用 @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; HashDbItemEntry that = (HashDbItemEntry) o; return Objects.equals(bucketKey, that.bucketKey) && Objects.equals(slotKey, that.slotKey) && Objects.equals(value, that.value); } @Override public int hashCode() { return Objects.hash(bucketKey, slotKey, value); } @Override public String toString() { return "HashDbItemEntry{" + "bucketKey='" + bucketKey + '\'' + ", slotKey='" + slotKey + '\'' + ", value=" + value + '}'; } }}
如上例子,展示了两种缓存,一种是 简单的 string -> string 的缓存, 另一种是 (string, string) -> byte[] 的缓存; 不管怎么样,只是想说明,缓存的方式有多种!
我们就以简单的 string -> string 来说明吧!
stringDbCacheContainer = CacheBuilder.newBuilder() .expireAfterWrite(guavaCacheTimeout, TimeUnit.SECONDS) .maximumSize(maxCacheSize) .build(new CacheLoader() { @Override public String load(String key) throws Exception { log.info("【缓存】从redis获取配置:{}", key); String value = redisTemplate.get(key); return StringUtils.defaultIfBlank(value, EMPTY_VALUE_STRING); } });
如上,咱们创建了一个缓存容器,它的最大容量是 maxCacheSize, 且每个key将在 guavaCacheTimeout 后过期, 过期后将从 redisTemplate 中获取数据!
如上,一个完整的 两级缓存组件就完成了,你大可以直接在项目进行相应的操作了!是不是很简单?
深入理解guava 二级缓存原理?
1. CacheBuilder 是如何创建的?
@GwtCompatible(emulated = true)public final class CacheBuilder{ private static final int DEFAULT_INITIAL_CAPACITY = 16; private static final int DEFAULT_CONCURRENCY_LEVEL = 4; private static final int DEFAULT_EXPIRATION_NANOS = 0; private static final int DEFAULT_REFRESH_NANOS = 0; static final Supplier NULL_STATS_COUNTER = Suppliers.ofInstance( new StatsCounter() { @Override public void recordHits(int count) {} @Override public void recordMisses(int count) {} @Override public void recordLoadSuccess(long loadTime) {} @Override public void recordLoadException(long loadTime) {} @Override public void recordEviction() {} @Override public CacheStats snapshot() { return EMPTY_STATS; } }); static final CacheStats EMPTY_STATS = new CacheStats(0, 0, 0, 0, 0, 0); static final Supplier CACHE_STATS_COUNTER = new Supplier () { @Override public StatsCounter get() { return new SimpleStatsCounter(); } }; enum NullListener implements RemovalListener
如上,使用建造者模式创建 LoadingCache<K, V> 缓存; 设置好 最大值,过期时间等参数;
2. 如何获取一个guava缓存?
其实就是一个get方法而已! stringDbCacheContainer.get(key);
// com.google.common.cache.LocalCache // LoadingCache methods @Override public V get(K key) throws ExecutionException { // 两种数据来源,一是直接获取,二是调用 load() 方法加载数据 return localCache.getOrLoad(key); } // com.google.common.cache.LocalCache V getOrLoad(K key) throws ExecutionException { return get(key, defaultLoader); } V get(K key, CacheLoader loader) throws ExecutionException { int hash = hash(checkNotNull(key)); // 还记得 ConcurrentHashMap 吗? 先定位segment, 再定位 entry return segmentFor(hash).get(key, hash, loader); } SegmentsegmentFor(int hash) { // TODO(fry): Lazily create segments? return segments[(hash >>> segmentShift) & segmentMask]; } // 核心取数逻辑在此get 中 // loading V get(K key, int hash, CacheLoader loader) throws ExecutionException { checkNotNull(key); checkNotNull(loader); try { if (count != 0) { // read-volatile // don't call getLiveEntry, which would ignore loading values ReferenceEntry e = getEntry(key, hash); if (e != null) { // 如果存在值,则依据 ticker 进行判断是否过期,从而直接返回值,具体过期逻辑稍后再说 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 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(); } } // static class Segment extends ReentrantLock // 整个 Segment 继承了 ReentrantLock, 所以 LocalCache 的锁是依赖于 ReentrantLock 实现的 V lockedGetOrLoad(K key, int hash, CacheLoader loader) throws ExecutionException { ReferenceEntry e; ValueReference valueReference = null; LoadingValueReference 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 > table = this.table; int index = hash & (table.length() - 1); ReferenceEntry first = table.get(index); // 处理 hash 碰撞时的链表查询 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; } } // 如果是第一次加载,则先创建 Entry, 进入 load() 逻辑 if (createNewEntry) { loadingValueReference = new LoadingValueReference (); 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. // 同步加载数据源值, 从 loader 中处理 synchronized (e) { return loadSync(key, hash, loadingValueReference, loader); } } finally { // 记录未命中计数,默认为空 statsCounter.recordMisses(1); } } else { // The entry already exists. Wait for loading. // 如果有线程正在更新缓存,则等待结果即可,具体实现就是调用 Future.get() return waitForLoadingValue(e, key, valueReference); } } // 加载原始值 // at most one of loadSync.loadAsync may be called for any given LoadingValueReference V loadSync(K key, int hash, LoadingValueReference loadingValueReference, CacheLoader loader) throws ExecutionException { // loadingValueReference中保存了回调引用,加载原始值 ListenableFuture loadingFuture = loadingValueReference.loadFuture(key, loader); // 存储数据入缓存,以便下次使用 return getAndRecordStats(key, hash, loadingValueReference, loadingFuture); } // 从 loader 中加载数据, public ListenableFuture loadFuture(K key, CacheLoader loader) { stopwatch.start(); V previousValue = oldValue.get(); try { // 如果原来没有值,则直接加载后返回 if (previousValue == null) { V newValue = loader.load(key); return set(newValue) ? futureValue : Futures.immediateFuture(newValue); } // 否则一般为无过期时间的数据进行 reload, 如果 reload() 的结果为空,则直接返回 // 须重写 reload() 实现 ListenableFuture newValue = loader.reload(key, previousValue); if (newValue == null) { return Futures.immediateFuture(null); } // To avoid a race, make sure the refreshed value is set into loadingValueReference // *before* returning newValue from the cache query. return Futures.transform(newValue, new Function () { @Override public V apply(V newValue) { LoadingValueReference.this.set(newValue); return newValue; } }); } catch (Throwable t) { if (t instanceof InterruptedException) { Thread.currentThread().interrupt(); } return setException(t) ? futureValue : fullyFailedFuture(t); } } // com.google.common.util.concurrent.Uninterruptibles /** * Waits uninterruptibly for { @code newValue} to be loaded, and then records loading stats. */ V getAndRecordStats(K key, int hash, LoadingValueReference loadingValueReference, ListenableFuture newValue) throws ExecutionException { V value = null; try { // 同步等待加载结果,注意,此处返回值不允许为null, 否则将报异常,这可能是为了规避缓存攻击漏洞吧 value = getUninterruptibly(newValue); if (value == null) { throw new InvalidCacheLoadException("CacheLoader returned null for key " + key + "."); } // 加载成功记录,此处扩展点,默认为空 statsCounter.recordLoadSuccess(loadingValueReference.elapsedNanos()); // 最后将值存入缓存容器中,返回(论hash的重要性) storeLoadedValue(key, hash, loadingValueReference, value); return value; } finally { if (value == null) { statsCounter.recordLoadException(loadingValueReference.elapsedNanos()); removeLoadingValue(key, hash, loadingValueReference); } } } /** * Invokes { @code future.}{ @link Future#get() get()} uninterruptibly. * To get uninterruptibility and remove checked exceptions, see * { @link Futures#getUnchecked}. * * If instead, you wish to treat { @link InterruptedException} uniformly * with other exceptions, see { @link Futures#get(Future, Class) Futures.get} * or { @link Futures#makeChecked}. * * @throws ExecutionException if the computation threw an exception * @throws CancellationException if the computation was cancelled */ public static
V getUninterruptibly(Future future) throws ExecutionException { boolean interrupted = false; try { while (true) { try { return future.get(); } catch (InterruptedException e) { interrupted = true; } } } finally { if (interrupted) { Thread.currentThread().interrupt(); } } }
如上,就是获取一个缓存的过程。总结下来就是:
1. 先使用hash定位到 segment中,然后尝试直接到 map中获取结果;
2. 如果没有找到或者已过期,则调用客户端的load()方法加载原始数据; 3. 将结果存入 segment.map 中,本地缓存生效; 4. 记录命中情况,读取计数;3. 如何处理过期?
其实刚刚我们在看get()方法时,就看到了一些端倪。
要确认两点: 1. 是否有创建异步清理线程进行过期数据清理? 2. 清理过程中,原始数据如何自处?
其实guava的清理时机是在加载数据之前进行的!
// com.google.common.cache.LocalCache // static class Segmentextends ReentrantLock // 整个 Segment 继承了 ReentrantLock, 所以 LocalCache 的锁是依赖于 ReentrantLock 实现的 V lockedGetOrLoad(K key, int hash, CacheLoader loader) throws ExecutionException { ReferenceEntry e; ValueReference valueReference = null; LoadingValueReference 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 > table = this.table; int index = hash & (table.length() - 1); ReferenceEntry first = table.get(index); // 处理 hash 碰撞时的链表查询 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; } } // 如果是第一次加载,则先创建 Entry, 进入 load() 逻辑 if (createNewEntry) { loadingValueReference = new LoadingValueReference (); 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. // 同步加载数据源值, 从 loader 中处理 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); } } // 我们来细看下 preWriteCleanup(now); 是如何清理过期数据的 /** * Performs routine cleanup prior to executing a write. This should be called every time a * write thread acquires the segment lock, immediately after acquiring the lock. * * Post-condition: expireEntries has been run. */ @GuardedBy("this") void preWriteCleanup(long now) { runLockedCleanup(now); } void runLockedCleanup(long now) { // 再次确保清理数据时,锁是存在的 if (tryLock()) { try { // 当存在特殊类型数据时,可以先进行清理 drainReferenceQueues(); // 清理过期数据,按时间清理 expireEntries(now); // calls drainRecencyQueue // 读计数清零 readCount.set(0); } finally { unlock(); } } } /** * Drain the key and value reference queues, cleaning up internal entries containing garbage * collected keys or values. */ @GuardedBy("this") void drainReferenceQueues() { if (map.usesKeyReferences()) { drainKeyReferenceQueue(); } if (map.usesValueReferences()) { drainValueReferenceQueue(); } } @GuardedBy("this") void expireEntries(long now) { // 更新最近的访问队列 drainRecencyQueue(); ReferenceEntry
e; // 从头部开始取元素,如果过期就进行清理 // 写队列超时: 清理, 访问队列超时: 清理 while ((e = writeQueue.peek()) != null && map.isExpired(e, now)) { if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) { throw new AssertionError(); } } while ((e = accessQueue.peek()) != null && map.isExpired(e, now)) { if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) { throw new AssertionError(); } } } @Override public ReferenceEntry peek() { ReferenceEntry next = head.getNextInAccessQueue(); return (next == head) ? null : next; } // 清理指定类型的元素,如 过期元素 @GuardedBy("this") boolean removeEntry(ReferenceEntry entry, int hash, RemovalCause cause) { int newCount = this.count - 1; AtomicReferenceArray > table = this.table; int index = hash & (table.length() - 1); ReferenceEntry first = table.get(index); for (ReferenceEntry e = first; e != null; e = e.getNext()) { if (e == entry) { ++modCount; // 调用 removeValueFromChain, 清理具体元素 ReferenceEntry newFirst = removeValueFromChain( first, e, e.getKey(), hash, e.getValueReference(), cause); newCount = this.count - 1; table.set(index, newFirst); this.count = newCount; // write-volatile return true; } } return false; } @GuardedBy("this") @Nullable ReferenceEntry removeValueFromChain(ReferenceEntry first, ReferenceEntry entry, @Nullable K key, int hash, ValueReference valueReference, RemovalCause cause) { enqueueNotification(key, hash, valueReference, cause); // 清理两队列 writeQueue.remove(entry); accessQueue.remove(entry); if (valueReference.isLoading()) { valueReference.notifyNewValue(null); return first; } else { return removeEntryFromChain(first, entry); } } @GuardedBy("this") @Nullable ReferenceEntry removeEntryFromChain(ReferenceEntry first, ReferenceEntry entry) { int newCount = count; // 普通情况,则直接返回 next 元素链即可 // 针对有first != entry 的情况,则依次将 first 移动到队尾,然后跳到下一个元素返回 ReferenceEntry newFirst = entry.getNext(); for (ReferenceEntry e = first; e != entry; e = e.getNext()) { // 将first链表倒转到 newFirst 尾部 ReferenceEntry next = copyEntry(e, newFirst); if (next != null) { newFirst = next; } else { removeCollectedEntry(e); newCount--; } } this.count = newCount; return newFirst; }
到此,我们就完整的看到了一个 key 的过期处理流程了。总结就是:
1. 在读取的时候,触发清理操作;
2. 使用 ReentrantLock 来进行线程安全的更新; 3. 读取计数器清零,元素数量减少;3. 怎样主动放入一个缓存?
这个和普通的map的put方法一样,简单看下即可!
// com.google.common.cache.LocalCache$LocalManualCache @Override public void put(K key, V value) { localCache.put(key, value); } // com.google.common.cache.LocalCache @Override public V put(K key, V value) { checkNotNull(key); checkNotNull(value); int hash = hash(key); return segmentFor(hash).put(key, hash, value, false); } // com.google.common.cache.LocalCache$Segment @Nullable V put(K key, int hash, V value, boolean onlyIfAbsent) { lock(); try { long now = map.ticker.read(); preWriteCleanup(now); int newCount = this.count + 1; if (newCount > this.threshold) { // ensure capacity expand(); newCount = this.count + 1; } AtomicReferenceArray> table = this.table; int index = hash & (table.length() - 1); ReferenceEntry first = table.get(index); // Look for an existing entry. for (ReferenceEntry 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 valueReference = e.getValueReference(); V entryValue = valueReference.get(); if (entryValue == null) { ++modCount; if (valueReference.isActive()) { enqueueNotification(key, hash, valueReference, RemovalCause.COLLECTED); setValue(e, key, value, now); newCount = this.count; // count remains unchanged } else { setValue(e, key, value, now); newCount = this.count + 1; } this.count = newCount; // write-volatile evictEntries(); return null; } else if (onlyIfAbsent) { // Mimic // "if (!map.containsKey(key)) ... // else return map.get(key); recordLockedRead(e, now); return entryValue; } else { // clobber existing entry, count remains unchanged ++modCount; enqueueNotification(key, hash, valueReference, RemovalCause.REPLACED); setValue(e, key, value, now); evictEntries(); return entryValue; } } } // Create a new entry. ++modCount; ReferenceEntry newEntry = newEntry(key, hash, first); setValue(newEntry, key, value, now); table.set(index, newEntry); newCount = this.count + 1; this.count = newCount; // write-volatile evictEntries(); return null; } finally { unlock(); postWriteCleanup(); } }
就这样,基于guava的二级缓存功能就搞定了。
老话:感谢你遇到的每一个bug!