一种通用的简易缓存设计方案
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注解的保留构造方法,如此才能实现自动的序列化和反序列化。