程序员冈刀,目前就职于美团,java开发工程师,研究生。2022年,毕业于北京邮电大学电子工程学院、电子与通信工程专业。个人公众号《代码废柴》欢迎关注。
1. Java本地缓存的作用
本地缓存实际上就是将数据存储在自己的应用中,没有存储到其他的位置,例如:Redis等等。本地缓存的一大好处是处理速度极快而且不需要访问下游的数据,但是,它也存在缺点。当一个应用是集群的方式部署的时候,由于本地缓存信息缺乏信息共享的能力,所以,同一个数据会存在多个应用中,有可能造成结果的不一致性。但是,我们也是离不开本地缓存的,我们可以设计成下面这样,可以有效地解决网络延时问题。
今天给大家介绍的是本地缓存的设计,在这其中,会使用几种设计模式,例如:装饰者模式、单例模式等等。一共分为下面的4大部分:
- 定义缓存接口,用于阐述缓存的功能。
- 持久性缓存实现
- LRU缓存实现
- 带有版本的缓存实现
2. 缓存的具体实现
第一步:定义缓存接口,用于阐述缓存的功能。
/**
* 缓存接口
*
* @author breakpoint/赵先生
* create on 2021/02/03
*/
public interface Cache<T> {
// get ID
String getId();
// get SIZE
int getSize();
// 放入对象
void putObject(String key, T value);
// 获取到对象
T getObject(String key);
// 移除对象
T removeObject(String key);
// 清除所有的对象
void clear();
// 获取所有的缓存
List<T> getAll();
// 获取锁的结构
ReadWriteLock getReadWriteLock();
}
第二步:持久性缓存实现
持久化缓存实现的方式采用Map实现,同时为了多线程操作的问题,这里使用了读写锁操作数据的读取与更新。
获取读写锁这里(上面的图片),采用的是DCL单例模式。
第三步:LRU缓存实现在实现LRU缓存这里,使用了装饰者模式,如下面的delegate对象就是操作的真实对象。并且采用LinkedHashMap记录最近最久未使用的key,然后针对相应的数据进行移除。
第四步:带有版本时间的缓存实现这个实现带有时间失效的特点,但是如果在有效的时间内访问,就会重新设置这个值的有效时间。
在定义获取线程池操作时,这里采用的是自定义线程池,并且是单个有节线程池。同时采用内部静态类的方式实现单例模式。如下图:
在提交,创建缓存对象的时候才会创建线程池的实例,节省资源。
通过上面的一整个的操作流程,实现了定时的清除过期的数据,防止存在内存泄漏的风险。
在获取对象的时候,会动态地更新这个数据的有效时间。如上图。
在清理数据的时候,每清理5000个数据,这里给JVM一次机会进行垃圾回收,如上图。
3. 复盘总结
这个本地缓存的实现参考了Mybatis的本地缓存,并且在此基础上进行了优化,同时添加了带有版本缓存时间的本地缓存。在实现的过程中,使用了装饰者模式和DCL单例模式以及内部静态类单例模式。
由于作者水平有限,肯定存在许多考虑不周到的地方,欢迎大家一起讨论。
程序员冈刀,目前就职于美团,java开发工程师,研究生。2022年,毕业于北京邮电大学电子工程学院、电子与通信工程专业。个人公众号《代码废柴》欢迎关注。