一种通用的简易缓存设计方案
1,领域模型设计

该设计方案定义了三个基础接口: Cache,Cleanable和CacheManager;和一个默认实现类DefaultCacheManager。
- Cache接口抽象了非内存缓存所能提供的基础操作,Cache接口隔离了外部缓存的具体实现方案,可以是Redis/Codis等任意形式的缓存方案;
- Cleanable接口定义了被缓存对象(value)的基本属性,所以需要被缓存的对象都必须实现Cleanable接口;
- CacheManager抽象了该缓存方案支持的功能,直接提供给客户端调用,屏蔽了具体缓存实现方案,允许提供多个解决方案;
- DefaultCacheManager是CacehManager的实现类,支持使用本地内存和外部缓存的实现。
2,Cache interface

该接口的定义没有多少争议,就是实现序列化对象通过key进行存取。
3,Cleanable interface

前文已说明,所有需要被缓存的对象都必须实现该接口。接口定义了,缓存对象必须是可清理的,有过期时间: expiredTime,创建时间: createdTime;如果使用的是本地内存实现,则会通过isValid方法方便的检查对象是否过期,否则使用timeout方法来设置外部缓存时长。
4,CacheManager interface

CacheManager接口是真正面向客户端使用的,缓存的value是Cleanable对象,key允许是任何可序列化值(常用String);特别地,需要携带一个枚举类ModuleType。在微服务或分布式的架构系统中,无论是在项目初期或中后期,都可能公用同一个外部缓存服务器,因此为了避免缓存数据冲突和方便数据追踪,都需要对Key进行模块分割。
5,DefaultCacheManager
public class DefaultCacheManager implements CacheManager, Runnable {
private static ConcurrentMap<String, Cleanable> dataMap = new ConcurrentHashMap<String, Cleanable>();
private final Long cleanPeriod;
private final boolean enabledRedisCache;
private final ScheduledExecutorService validationService;
private final Cache cacheClient;
public DefaultCacheManager() {
this(1000L, null);
}
public DefaultCacheManager(Cache cacheClient) {
this(null, cacheClient);
}
public DefaultCacheManager(Long cleanPeriod) {
this(cleanPeriod, null);
}
/**
* @param cleanPeriod
* @param cacheClient
*/
private DefaultCacheManager(Long cleanPeriod, Cache cacheClient) {
this.enabledRedisCache = cacheClient != null ? true : false;
this.cacheClient = cacheClient;
this.cleanPeriod = cleanPeriod;
if(!enabledRedisCache) {
/**
* 在本地完成expired清理动作
*/
this.validationService = Executors.newSingleThreadScheduledExecutor();
this.validationService.scheduleAtFixedRate(this, this.cleanPeriod,
this.cleanPeriod, TimeUnit.MILLISECONDS);
} else
this.validationService = null;
}
@Override
public boolean exist(Serializable srcKey, ModuleType keyType) {
String key = keyType.key(srcKey);
if(enabledRedisCache) {
return cacheClient.getValue(key) != null;
}
Cleanable c = dataMap.get(key);
if (c == null)
return false;
if (!c.isValid()) {
dataMap.remove(key);
return false;
}
return true;
}
@Override
public void put(Serializable srcKey, ModuleType keyType, Cleanable value) {
String key = keyType.key(srcKey);
if(enabledRedisCache) {
cacheClient.setValue(key, value, value.timeout(), TimeUnit.SECONDS);
return;
}
dataMap.put(key, value);
}
@Override
public Cleanable get(Serializable srcKey, ModuleType keyType) {
String key = keyType.key(srcKey);
Cleanable value;
if(enabledRedisCache) {
value = ((Cleanable) cacheClient.getValue(key));
} else {
value = dataMap.get(key);
}
return value;
}
@Override
public void remove(Serializable srcKey, ModuleType keyType) {
String key = keyType.key(srcKey);
if(enabledRedisCache) {
cacheClient.removeValue(key);
} else {
dataMap.remove(key);
}
}
public void run() {
/**
* 清理过期对象
*/
Iterator<String> iter = dataMap.keySet().iterator();
while (iter.hasNext()){
String key = iter.next();
// 由于并发缘故
// 可能已经把该{@param key}对应的对象, 清理掉了
Cleanable c = dataMap.get(key);
if(c != null && !c.isValid()){
iter.remove();
}
}
}
}
|
DefaultCacheManager类是支持内存缓存和外部缓存的默认实现类。在此,可能会有疑惑,像Redis这样的外部缓存,应用已经非常普遍,为什么还要做内存缓存?我个人有三个考虑:1)即便是开源项目,如Eurka Server也仍然使用内存缓存,具体原因可查阅该项目的源码设计分析;2)对于微服务或分布式架构的系统,在项目早期或对于某些服务而言,需缓存数据量小或不需要共享缓存数据;3)追求高响应时间,不想额外依赖外部服务,提高系统可靠性。
6,一个实现Cleanable接口的案例
/** |
如上代码所示,Piece类是一个可缓存对象的实现类,其意义用于缓存所有请求下载过该资源片的设备节点列表。
a, 仔细看Piece类的属性有一个明显的特征,即所有属性都用final关键字修饰了;而实际上,所有Cleanable接口的实现类(或会被缓存的对象)的字段,除非必须的,都应当使用final关键字修饰。因为我们服务处在并发环境下,缓存对象应当尽可能做到线程安全,final修饰的作用就是消除JMM的重排序,保证创建的对象是线程安全的;而如果被缓存对象存在非final修饰的属性,则在发生修改时,若要确保数据的一致性,则必须加锁。
b, 另外,可以注意到Piece类有两个构造方法,一个是公开访问,一个是不可公开调用但可以通过反射工具调用的;所以,所有Cleanable的实现类都必须实现一个@JsonCreator注解的保留构造方法,如此才能实现自动的序列化和反序列化。
浙公网安备 33010602011771号