在java里基于jvm内存实现带过期检查的缓存组件

用了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缓存组件实现

同学们还可以关注公众号【暴走的怪兽君】
回复“缓存”下载这个组件源码以及更多资源内容

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

暴走的怪兽君

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值