实现目标
基础缓存就是实现一个缓存该有的功能,在这个版本中先不会考虑性能问题(性能将在之后的版本进行优化)。
我们暂时定义的功能有如下:
- 内存级缓存
- 缓存过期
- 无缓存存在则根据数据源自动载入数据
设计思路
- 定义Cache接口
- 定义数据源加载辅助接口
- 定义内存级缓存实现类,并组合缓存过期辅助类
构建骨架
接下来,根据我们的思路构建一个缓存架构的骨架。
构建Cache接口
由于需要针对不同的应用场景,所以对于不同的应用场景需要有不同的缓存实例。
在这里,参考guava和jodd的缓存实现,定义了不同对象类型的缓存实例。即定义Cache<K, V>泛型接口。在该接口内包含如下方法:
| 12
 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方法
由于存入缓存是写操作,我们需要保证在写的过程中,读线程处于阻塞状态。
| 12
 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来辅助获取原数据。
| 12
 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的实现则返回空 -> 如果存在则返回内容 -> 如果已经过期或者不存在缓存 -> 获取写锁 -> 重新载入原数据 -> 释放锁
至于如果通过缓存对象获取缓存的值,如下:
| 12
 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方法更改如下:
| 12
 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(),这个的判断逻辑是在缓存对象内实现:
| 12
 3
 4
 5
 6
 
 | if (ttl == 0) {
 return false;
 }
 
 return lastAccess + ttl < System.currentTimeMillis();
 
 | 
定时清理
定时清理的机制稍微有点麻烦,笔者在这里通过一个辅助类来实现这个机制。
通过辅助类实现这个机制的原因是:将该清理操作能够应用于所有实现了Cache接口的缓存实例,并由缓存实现自己决定是否需要定时清理这个机制。
| 12
 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架构
分析与计划
基础缓存