实现目标
基础缓存就是实现一个缓存该有的功能,在这个版本中先不会考虑性能问题(性能将在之后的版本进行优化)。
我们暂时定义的功能有如下:
- 内存级缓存
- 缓存过期
- 无缓存存在则根据数据源自动载入数据
设计思路
- 定义Cache接口
- 定义数据源加载辅助接口
- 定义内存级缓存实现类,并组合缓存过期辅助类
构建骨架
接下来,根据我们的思路构建一个缓存架构的骨架。
构建Cache接口
由于需要针对不同的应用场景,所以对于不同的应用场景需要有不同的缓存实例。
在这里,参考guava和jodd的缓存实现,定义了不同对象类型的缓存实例。即定义Cache<K, V>
泛型接口。在该接口内包含如下方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
|
void put(K key, V value, long timeout);
Optional<V> get(K key, CacheLoader<V> loader) throws InterruptedException, CacheLoaderFailedException;
boolean remove(K key);
boolean clearAll();
int size();
Set<K> keys();
boolean contains(K key);
Map<K, CacheObject<K, V>> snapshot();
|
上述基本包含了缓存需要的所有操作,最重要的其实就是获取与存入这两个方法。
实现内存级缓存
根据定义的缓存接口,就可以实现内存级缓存了。
现在有个问题,该将缓存存入到哪种容器内?笔者在这里是通过 HashMap
来存储缓存内容,并通过 StampedLock
锁 来保证并发情况下的数据共享问题。
JDK本身其实有并发的Map
集合ConcurrentHashMap
。不过,在实现缓存的过程中需要使用各种复合操作,而其本身的复合操作并不一定能够完全满足以后的功能扩展(ConcurrentHashMap
无法在客户端级别加锁保证独占式访问,详细可以看这篇文章);所以,直接选择了HashMap
+ StampedLock
的形式来保证多线程访问,这样,以后对于一些新功能易于扩展(以后可以考虑参照JDK8的ConcurrentHashMap
直接实现自己的容器,例如像guava就是参照了JDK7的ConcurrentHashMap
的分段锁原理)。
确定好容器之后,接下来就该实现我们的put
和get
方法了。
put
方法
由于存入缓存是写操作,我们需要保证在写的过程中,读线程处于阻塞状态。
1 2 3 4 5 6 7 8 9 10 11 12
| long stamp = stampedLock.writeLock();
try { CacheObject<K, V> cacheObject = CacheObject.createValueCache(key, value, timeout); cacheMap.put(key, cacheObject); } finally { stampedLock.unlockWrite(stamp); }
|
如上代码,笔者在执行缓存对象的创建与存入之前,做了一步加锁操作。加锁操作,一个是为了防止HashMap
在多线程环境下造成的死循环异常,再者也是为了防止出现size
、get
这些方法在多线程环境下数据不准确的情况,而这个很可能导致缓存击穿的事情发生,这样缓存也就没有意义了。
get
方法
我们在获取缓存的时候,如果出现缓存为空的情况,则需要通过CacheLoader
来辅助获取原数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| Asserts.notNull(loader);
long stamp = stampedLock.readLock();
try { CacheObject<K, V> obj = cacheMap.get(key); if (obj == null && loader instanceof CacheLoader.EmptyCacheLoader) { return Optional.empty(); }
if (obj == null) { stamp = stampedLock.tryConvertToWriteLock(stamp);
if (stamp == 0L) { stamp = stampedLock.writeLock(); }
FutureTask<V> futureTask = new FutureTask<>(loader::call); obj = CacheObject.createFutureCache(key, futureTask, 0L); cacheMap.replace(key, obj); }
return obj.getObject(); } catch (ExecutionException e) { throw new CacheLoaderFailedException(e); } finally { stampedLock.unlock(stamp); }
|
大致思路其实是:读锁锁定 -> 如果不存在且无CacheLoader
的实现则返回空 -> 如果存在则返回内容 -> 如果已经过期或者不存在缓存 -> 获取写锁 -> 重新载入原数据 -> 释放锁
至于如果通过缓存对象获取缓存的值,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| lastAccess = System.currentTimeMillis();
accessCount++;
if (futureResult == null) { return Optional.ofNullable(result); }
if (!futureResult.isDone()) { futureResult.run(); }
return Optional.ofNullable(futureResult.get());
|
缓存过期
针对于缓存过期的问题,笔者这里设计了两种实现方式。
- 懒过期:在获取的时候,同时判断缓存是否已经过期。保证能够实时移除过期缓存。
- 定时清理:启动一个清理线程,对于已经过期的缓存,做删除操作,防止存储的失效缓存过大的问题。
懒过期
懒过期的机制其实很好实现,只要在获取的时候,判断下缓存对象是否过期即可,我们将get
方法更改如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| Asserts.notNull(loader);
long stamp = stampedLock.readLock();
try { CacheObject<K, V> obj = cacheMap.get(key); if (obj == null && loader instanceof CacheLoader.EmptyCacheLoader) { return Optional.empty(); }
if (obj == null || obj.isExpired()) { ① stamp = stampedLock.tryConvertToWriteLock(stamp);
if (stamp == 0L) { stamp = stampedLock.writeLock(); }
FutureTask<V> futureTask = new FutureTask<>(loader::call); obj = CacheObject.createFutureCache(key, futureTask, 0L); cacheMap.replace(key, obj); }
return obj.getObject(); } catch (ExecutionException e) { throw new CacheLoaderFailedException(e); } finally { stampedLock.unlock(stamp); }
|
即在 ① 的代码处,增加一个或判断|| obj.isExpired()
,这个的判断逻辑是在缓存对象内实现:
1 2 3 4 5 6
| if (ttl == 0) { return false; }
return lastAccess + ttl < System.currentTimeMillis();
|
定时清理
定时清理的机制稍微有点麻烦,笔者在这里通过一个辅助类来实现这个机制。
通过辅助类实现这个机制的原因是:将该清理操作能够应用于所有实现了Cache接口的缓存实例,并由缓存实现自己决定是否需要定时清理这个机制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| final class CacheTimeoutHelper<K, V> {
private final Cache<K, V> cache; private final long delay; private final ScheduledExecutorService executor; private volatile boolean started = false;
CacheTimeoutHelper(Cache<K, V> cache, long delay) { this.cache = cache; this.delay = delay; this.executor = new ScheduledThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), new ThreadPoolExecutor.DiscardOldestPolicy()); }
public void start() { started = true; executor.schedule(() -> { for (K key : cache.keys()) { try { Optional<V> value = cache.get(key); if (!value.isPresent()) { cache.remove(key); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }, delay, TimeUnit.SECONDS); }
public boolean isStarted() { return started; }
}
|
总结
总体实现了缓存的基本功能,可能在性能上以及代码逻辑上存在一些可优化的地方,后期将会慢慢调整优化。
以下是Github的仓库地址:
https://github.com/LightweightJava/cache
欢迎各位给star或者和贡献代码。
一起写个Cache架构
分析与计划
基础缓存