缓存系统系统的作用不用多说,大概就是用于保存计算代价或者使用量比较高的数据,让下一次获取更快速或者代价更小。对于一个缓存系统应该具有如下功能:
- 实现缓存过期功能
- 重启不丢失数据
- 容灾内力(断电)
接下来具体实现这些功能。首先规定Cahce的接口
package com.getqiu.history.cache;
import java.io.Serializable;
/**
* 一个缓存
* */
public interface Cache<T extends Serializable> extends Serializable{
/**
* 获取缓存名称
* */
public String getCacheName();
/**
* 获取缓存内容
* */
public T getCacheElement();
/**
* 上次修改时间
* */
public long getLastModified();
/**
* 获得过期时间
* */
public long getExpireTime();
/**
* 缓存生命时间长度
* */
public long getDuration();
}
缓存要求所存放的元素都应该是可序列化的,方便后期保存,另外缓存自身也应该是可序列化的。因为一个一个缓存对象可能同时被多个线程访问,如果多个线程同时修改这个缓存,那么可能引发线程安全的问题,所以本系统仿照String对象的设计,将其设置成为一个不可变类。实现代码如下:
package com.getqiu.history.cache;
import java.io.Serializable;
/**
* Standard Cache,不可变对象,线程安全
* */
public final class StandardCache<T extends Serializable> implements Cache<T>,Serializable{
private static final long serialVersionUID = 6721431932201856975L;
private final long duration; //单位为毫秒
private final long lastModified;
private final long expire;
private static final long DEFAULT_CACHE_LIFE= 1000*60; //默认缓存一分钟
private static final Object DEFAULT_CACHE_ELEMENT = null;
private final String cacheName;
private final Object cacheElement;
private final Object defaultCacheElement;//当缓存已经失效后的默认值
public StandardCache(String cacheName,T cacheValue){
this(cacheName,cacheValue,DEFAULT_CACHE_LIFE);
}
@SuppressWarnings("unchecked")
public StandardCache(String cacheName,T cacheValue,long duration){
this(cacheName, cacheValue,duration,(T) DEFAULT_CACHE_ELEMENT);
}
public StandardCache(String cacheName,T cacheValue,T defaultCacheElement){
this(cacheName, cacheValue,DEFAULT_CACHE_LIFE,defaultCacheElement);
}
public StandardCache(String cacheName,T cacheValue,long duration,T defaultCacheElement){
this.cacheName = cacheName;
this.cacheElement = cacheValue;
this.duration = duration;
this.lastModified = System.currentTimeMillis();
this.expire = this.duration+lastModified;
this.defaultCacheElement = defaultCacheElement;
}
@Override
public String getCacheName(){
return this.cacheName;
}
@SuppressWarnings("unchecked")
@Override
public T getCacheElement() {
//检查缓存是否已经失效了,失效了返回null就好,让后台清理线程来清理过期缓存
return System.currentTimeMillis() < expire ? (T) cacheElement : (T) defaultCacheElement;
}
@Override
public long getLastModified() {
return lastModified;
}
@Override
public long getExpireTime() {
return expire;
}
@Override
public long getDuration() {
return duration;
}
@Override
public String toString(){
return "[" + cacheName +",expire after "+(expire-System.currentTimeMillis())/1000 +"s]";
}
}
如上standarCache实现,这是一个不可变类,除了可以通过构造方法以外,不能通过其他方式改变Cache当中的状态。在上面的实现中,尤其要注意getCacheElement()方法
@SuppressWarnings("unchecked")
@Override
public T getCacheElement() {
//检查缓存是否已经失效了,失效了返回null就好,让后台清理线程来清理过期缓存
return System.currentTimeMillis() < expire ? (T) cacheElement : (T) defaultCacheElement;
}
在这里,采用被动失效的方式实现了缓存的失效处理,判断当前的时间是否超过过期时间,如果超过了,返回默认缓存的对象,通常为null。这样就实现了缓存的自动失效。但是缓存对象依然存在于容器中,没有被清除,但是这是缓存的容器的事情。
缓存容器
public interface CacheContainer {
/**
* 初始化动作,起码是要恢复原先保存毫的cache,另外启动过期清理线程
* */
public void init();
/**
* 关闭cache的动作,保存内存中的Cache,关闭清理线程等等
* */
public void beforeDestory();
/**
* 获取其中的一个Cache
* */
public Cache<?> findCache(String key);
/**
* 添加一个cache
* */
public void addCache(Cache<? extends Serializable> cache);
/**
* 删除一个cache
* */
public void removeCache(String key);
/**
* 使得一个cache立即过期,其实和remove方法一样的作用
* */
public void expireCache(Cache<? extends Serializable> cache);
缓存容器有生命周期方法init()和beforeDestory()方法,这些两个方法主要完成的工作如注释所示。
接下来看具体的实现:
public abstract class AbstractMemCacheContainer implements CacheContainer{
@Override
public void init() {
loadCacheFromDisk();
recoveryFromSnapshot();
startBackgroundThreads();
}
@Override
public void beforeDestory() {
stopBackgroundThreads();
persistCacheToDisk();
}
}
在容器启动的最初,执行init()方法,在init()方法中,loadCacheFromDisk()将正常关机前保存的cache重新加载到内存当中。recoveryFromSnapshot()方法主要检查上次关机是否正常关机,如果不是正常关机,那么将从快照中恢复数据。最后启动后台线程,后台线程主要的工作是定期清理过期的cache,以及找时间做数据的快照。
而destory()方法主要将在关机之前关闭后台线程,并且将内存中的缓存持久化到磁盘中。
做快照和关机开机之前的持久化和从磁盘加载缓存就是一个序列化和反序列化的过程,在前面可以看到,Cache实现了Serializable接口,因此可以被序列化,这里展示做快照的过程。
/**
* container当中在时间段内修改添加删除的次数,用于决定是否对cache进行快照
* */
protected AtomicLong changeTime = new AtomicLong();
/**
* 在制定时间内修改,添加,删除个数超过这个值,那么下次快照时将会快照缓存
* */
protected int snapshotOnChange = 2;
/**
* 这个工作需要额外的线程单独跑
* */
public void snapshot(){
if(changeTime.get() >= snapshotOnChange){
// 先将文件写再一个临时文件中,写完了在重命名过去
Path destDir = Paths.get(memCachePath.toString(), SNAPSHOT_FILE_NAME+".tmp");
try(
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(destDir.toFile()))
){
Integer size = memCacheMap.size();
oos.writeObject(size);//先记录一下有多少个对象需要序列化
Iterator<Entry<String, Cache<? extends Serializable>>> iterator = memCacheMap.entrySet().iterator();
while(iterator.hasNext()){
oos.writeObject(iterator.next().getValue());//将Cache对象序列化
}
changeTime.set(0); //归零
}
catch (Exception e) {
e.printStackTrace();
}
Path originPath = Paths.get(memCachePath.toString(), SNAPSHOT_FILE_NAME);
destDir.toFile().renameTo(originPath.toFile());
logger.debug("created snapshot for cache container....");
}
else{
logger.debug("the cache container does not change much,it's not nessary to create snapshot");
}
}
这里用了一个protected AtomicLong changeTime = new AtomicLong(); 线程安全的long变量来记录当前缓存容器变化的次数。如果在指定时间内变化超过一定次数(snapshotOnChange)才会做快照。快照的过程就是将当前容器中的缓存都序列化到磁盘上。但是在写快照的时候,应该将快照先写到一个交换文件上,写完了再重命名过去,这样可以防止在做快照的过程当中遇到断电造成快照文件损坏。
剩下的的就是做缓存失效了,刚才在cache的地方做了被动失效,但是缓存并没有从容器当中删除,这个职责应该容器来完成。
清理缓存的思路比较简单,就是遍历容器中的缓存对象,检查是否过期,如果过期就将它从缓存容器中删除。代码如下:
/**
* 定期清理过期的Cache,使用别的线程来运行这个方法。
* */
public void cleanExpiredCache(){
Iterator<Entry<String, Cache<?>>> iterator = memCacheMap.entrySet().iterator();
while(iterator.hasNext()){
Entry<String, Cache<?>> aPiceOfCache = iterator.next();
if(System.currentTimeMillis() >= aPiceOfCache.getValue().getExpireTime()){
logger.info("xxxx expire "+aPiceOfCache);
iterator.remove();
changeTime.getAndIncrement();
}
}
}
一般来说,缓存容器在全局都只有一个,所以可以将这个对象设置成为单列。
恩,基本的功能就差不多这些。更具体的实现方式参见我的github项目当中的com.getqiu.history.cache
包下的类