用了redis,是不是很喜欢redis的过期机制,带时效性的数据存进去,不需要在代码中处理数据就自动过期删除。
那么,基于redis的使用场景,怎么在Java中实现并增强这一机制呢?接下来我将和大家分享我自己的一个基于jvm内存打造的缓存组件。
第一步:定义存储结构-键值存储
键值存储在java中最直接简单的方式就是HashMap,高效直接,所以组件底层核心存储结构就是一个基于HashMap的散列存储。
public class LocalCache{
private Map<String,CacheNode> nodeMap=new HashMap<>();
public void cache(String key,Object obj){...}
public Object get(String key){...}
public void drop(String key{...}
}
然后在组件中写出对这个map的put、get、drop等方法,当然,如果为了符合咱们缓存的特性,put可以改名为cache更形象一点。
同时,这里的get我们可以优化一下,虽然存储的时候是按照Object存储,但在get时调用方可不是想要一个Object,咱们可以结合泛型做一些语法糖的优化处理
// 泛型返回一定要注意类型转换错误,可能存储的数据类型本来就不是调用方想要的数据类型
public <T> T get(String key){...}
第二步:定义存储封装对象
聪明的同学可能从上面中看到了map中的value并非简单的Object对象,而是一个未知的CacheNode,那么,这个CacheNode是什么呢?咱们接着往下看。
@Data
@Accessors(chain = true)
public static class CacheNode{
private String key;
private Object data;
private long expire;
}
大家是否记得,咱们这个组件是带有过期机制的,所以每个存储的对象都有自己的过期时间,所以在存储的时候,上面的cache方法不仅需要传入数据对象,还需要传入对象的有效期。
public void cache(String key,Object obj,long expire){...}
当然,作为一个合格的组件,怎么能只提供一个cache方法呢,咱们来给他重载一下
public void cache(String key,Object obj){
// 这里的defaultExpire由各位在开发时时自定义
cache(key,obj,defaultExpire)
}
public void cache(String key,Object obj,long expire){
// 缓存的具体操作,封装为CacheNode存入map
}
第三步:过期检查
作为一个合格的简单版redis组件,过期检查怎么能少呢?
过期检查有两种方案,一种是懒式检查,一种是积极检查,咱们一个一个说
方案一:懒式检查
所谓懒式检查,就是在需要的时候才检查,我们可以在这个数据被查询的时候检查,过期了就返回null
当然如果只是查询的时候检查被查询的数据,这样就会导致过多的死数据存储在组件里无法被检查清楚,所以,我们还需要每隔一段时间检查所有的存储数据,话不多说,看实现。
/**
* 获取缓存
* @param key
* @param clean 是否获取后清理该缓存
* @param <T>
* @return
*/
public <T> T get(String key){
// 懒式清理会在每一次get时检查一次清理间隔,符合清理间隔则清理
lazyClean();
CacheNode node=null;
synchronized (locker){
node=nodeMap.get(key);
}
node=validExpired(node,System.currentTimeMillis());
if(node==null){
return null;
}
try{
// 尝试转换为目标类型
T target= (T) node.getData();
return target;
}catch (Exception e){
// 转换失败
return null;
}
}
/**
* 懒式清理
*/
private void lazyClean(){
// cleanInterval:全局清理间隔,实现时自定义即可
if(System.currentTimeMillis()-lastCleanTime<cleanInterval){
return;
}
long time=System.currentTimeMillis();
Collection<CacheNode> nodes=new ArrayList<>();
synchronized (locker){
nodes.addAll(nodeMap.values());
}
for(CacheNode node:nodes){
validExpired(node,time);
}
lastCleanTime=time;
}
/**
* 检查节点是否过期
* @param node
* @param time
* @return
*/
private CacheNode validExpired(CacheNode node,long time){
if(node==null){
return null;
}
// 过期时间小于当前时间,存储到期
if(node.getExpire()>=time){
return node;
}
synchronized (locker){
nodeMap.remove(node.getKey());
}
if(node.getCallback()!=null){
node.getCallback().onTimeout(node.getData());
}
return null;
}
这样,在每次查询时,不仅会对单个节点进行过期检查,还会定期对所有数据进行过期检查。
方案二:积极检查
积极检查的方案就很简单了,设定一个时间,在组件内部运行一个进行检查的Thread,定时对所有数据进行检查。
/**
* 缓存清理器
*/
private class CacheCleaner extends Thread implements Closeable{
private boolean running=true;
private long interval=20*1000;
public CacheCleaner(long interval){
this.interval=interval;
}
@Override
public void run() {
while(running){
// 一定要注意捕捉异常,否则一旦抛出异常会导致整个run方法结束
try{
cleanCache();
}catch (Exception e){}
try {
Thread.sleep(interval);
} catch (InterruptedException e) {}
}
}
@Override
public void close() throws IOException {
running=false;
}
}
两种方案优缺点分析:
1.懒式检查没有后台线程维护,性能节省由于积极检查,但缺点也很明显,如果一时间存入大量数据然后由长期没有查询操作,会导致数据堆积无法清除,同时在get时如果碰到清理节点效率底下,get效率无法保持一致。
2.积极检查和懒式检查相反,多了一个线程的维护开销,对数据定期检查,同时在get时对被查询的数据核对检查,在get时的效率几乎保持一致。
第四步:过期处理
这一步时对过期的增强处理,可能有的同学会有疑问,过期了删掉不就好了吗,干嘛还处理呢?
的确,在普通的缓存组件中,过期失效了扔掉就行了,但从业务角度来看,很多数据过期了需要告知调用方,调用方可能会有进一步的其他处理。也就是说,我们为了增强这一场景,在数据过期的时候可以回调调用方的某个方法达到通知的目的。
既然要回调,那么调用方在存储的时候就需要将回调函数传入,我们需要修改一下我们的CacheNode来存储这个回调函数
@Data
@Accessors(chain = true)
public static class CacheNode{
private String key;
private Object data;
private long expire;
private CacheCallback callback;
}
// 回调接口
public interface CacheCallback{
void onTimeout(Object data);
void onDrop(Object data);
}
在回调接口里,定义了两个回调方法,一个是超时回调,一个是数据被删除回调,这里的删除指的是主动调用drop方法删除,而过期清理则回调timeout方法。
我们的cache方法也得跟着改一改
public void cache(String key,Object obj,long expire,CacheCallback callback){
// 封装CacheNode存入map
}
// 当然,我们依然对cache进行多参数重载
public void cache(String key,Object obj){...}
public void cache(String key,Object obj,long expire){...}
public void cache(String key,Object obj,CacheCallback callback){...}
就这样,我们一个完整的简单缓存组件就完成了,在平时业务开发中,有些使用为了单一的缓存场景去使用redis会显得太重了,这种简单轻量级的缓存组件也能满足比较常用的业务场景了。
最后
完整的代码文件我放在了资源文件里,大家可以点击下载查看:LocalCache缓存组件实现
同学们还可以关注公众号【暴走的怪兽君】
回复“缓存”下载这个组件源码以及更多资源内容