目录
0.写在前面的话
本篇介绍一种支持调度的键值缓存设计,其中缓存的对象是一个Callable对象;
主要从顶层接口、缓存实现、自定义缓存调度、线程安全性保证、后续改进等几个角度进行说明;
缓存设计的类图如下:
1.顶层接口
顶层接口从上层需求角度定义了一套逻辑接口方法,用程序语言定义了该模块是什么,能够干什么事情,具体怎么干交由具体的子类去实现;它是对底层实现的重要参考和规范,体现了一种由抽象到具体、由简单到复杂的过程。
1.1顶层接口KVCache<K, V>
顶层接口KVCache<K, V>定义了几个顶层缓存方法,主要用于增删查等操作,源码如下:
public interface KVCache<K, V> {
/**
* @return The identifier of this cache
*/
String getId();
/**
* putObject
*
* @param key The key
* @param value The put value
*/
void putObject(K key, V value);
/**
* @param key The key
* @return The object stored in the cache.
*/
V getObject(K key);
/**
* @param key The key
*/
void removeObject(K key);
/**
* Clears this cache instance
*/
void clear();
/**
* @return The number of elements stored in the cache (not its capacity).
*/
int getSize();
}
1.2顶层接口CallableCapturer<K, V>
该顶层接口主要提供了一个可由子类去具体实现的获取Callable的接口方法,最大程度上满足不同子类具体实现的差异性需求;
public interface CallableCapturer<K, V> {
/**
* Let subclass implement this method to specify concrete action
*
* @param key The key
* @return
*/
Callable<V> getCallable(K key);
}
2.缓存实现AbstractCallableKVCache<K, V>
AbstractCallableKVCache<K, V>对KVCache<K, V>顶层接口进行了具体实现,id表示该缓存的唯一标识,ConcurrentMap用于具体存储需要缓存的键值对;
同时实现CallableCapturer<K, V>接口,主要用于从具体子类中获取构造的Callable对象;
@ThreadSafe
public abstract class AbstractCallableKVCache<K, V> implements CallableCapturer<K, V>, KVCache<K, V> {
private static final Logger log = LoggerFactory.getLogger(AbstractCallableKVCache.class);
private final String id;
private final ConcurrentMap<K, Future<V>> cache = Maps.newConcurrentMap();
protected AbstractCallableKVCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public void putObject(K key, V value) {
cache.put(key, new FutureTask(() -> value));
}
@Override
public V getObject(K key) {
Future<V> origin = cache.get(key);
if (origin == null) {
Future<V> futureTask = new FutureTask(getCallable(key));
origin = cache.putIfAbsent(key, futureTask);
if (origin == null) {
origin = futureTask;
((FutureTask) futureTask).run();
}
} else {
log.info("Hit Local Cache Key:{}", key);
}
try {
return origin.get();
} catch (CancellationException e) {
log.error("ThreadSafeCache#getObject - Task is cancelled exception, key:{}", key, e);
cache.remove(key, origin);
throw new KVCacheRuntimeException("Task is cancelled exception", e);
} catch (InterruptedException e) {
log.error("ThreadSafeCache#getObject - Thread is interrupted exception, key:{}", key, e);
Thread.currentThread().interrupt();
cache.remove(key, origin);
throw new KVCacheRuntimeException("Thread is interrupted exception", e);
} catch (ExecutionException e) {
log.error("ThreadSafeCache#getObject -An execution exception is occurred, key:{}", key, e);
cache.remove(key, origin);
throw new KVCacheRuntimeException("An execution exception is occurred", e);
}
}
@Override
public void removeObject(K key) {
cache.remove(key);
}
@Override
public void clear() {
cache.clear();
}
@Override
public int getSize() {
return cache.size();
}
}
3.自定义缓存调度ScheduledKVCache<K, V>
ScheduledKVCache<K, V>包装了缓存失效策略的实现,定义了一个超时时间,当到达超时时间的时候清空本地缓存,具体实现如下:
这里主要参考mybatis提供的ScheduledCache设计实现(ScheduledCache不支持多线程访问);
public class ScheduledKVCache<K, V> implements KVCache<K, V> {
private static final Logger log = LoggerFactory.getLogger(ScheduledKVCache.class);
private final KVCache<K, V> delegate;
private volatile long lastClear;
private volatile long clearInterval;
public ScheduledKVCache(KVCache<K, V> delegate) {
this.delegate = delegate;
this.clearInterval = 10 * 60 * 1000L; //default 10 minutes
this.lastClear = System.currentTimeMillis();
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public void putObject(K key, V value) {
clearWhenStale();
delegate.putObject(key, value);
}
@Override
public V getObject(K key) {
clearWhenStale();
return delegate.getObject(key);
}
@Override
public void removeObject(K key) {
clearWhenStale();
delegate.removeObject(key);
}
@Override
public void clear() {
log.info("!----------------------Clear Local Cache----------------------!");
lastClear = System.currentTimeMillis();
delegate.clear();
}
@Override
public int getSize() {
clearWhenStale();
return delegate.getSize();
}
private boolean clearWhenStale() {
if (System.currentTimeMillis() - lastClear > clearInterval) {
clear();
return true;
}
return false;
}
public void setClearInterval(long clearInterval) {
this.clearInterval = clearInterval;
}
}
4.线程安全性保证
这里线程安全性主要包含如下几点保障:
-
委托给ConcurrentMap,ConcurrentMap利用分段锁实现了线程安全,同时保证了多线程的并发读写能力;
-
域变量修饰符为volatile,volatile保证了多线程下的可见性;
-
域变量修饰符为final,final保证了初始化过程的安全发布,实现线程安全性;
5.后续改进
- 目前实现的支持调度的键值缓存,在达到超时时间时,会把整个缓存清空,也就是说超时时间的作用粒度是在整个缓存;
- 后期可以支持超时时间的作用粒度时间到单个key上,不同key之间的超时时间是独立的,这样就能进一步的提升缓存的命中率,提升缓存性能;