1.简介
在大型企业网站项目中,由于高并发等因素,常常需要集成一些中间件,而缓存几乎是必不可少的,因为它对于性能提升非常巨大,比如说redis。但是我们不仅仅需要redis,因为redis存放的是可变的用户数据。我们常常会需要一种东西来存放我们系统的一些配置,这些数据不是动态的,基本不会改变的。如果我们把这些数据放入redis诚然是一个选择,但是当我们需要更改我们的配置时,要去redis更改,这是非常不方便的,redis没有控制台(当然可以自己做一个,但是不如接下来介绍的方便)。所以本地缓存就被大佬们造出来了。在项目上的做法是:把一些配置,存到数据库。然后用本地缓存去读取,这样当使用的时候,直接从本地缓存中拿,不用再去访问数据库,不仅提高了效率,还减轻了数据库的压力。不过却占用了内存空间。所以本地缓存不应该存储过多的数据(这里的过多一般指大于1万条,视情况而定)。
2.实现方案
首先看看我们对本地缓存的功能要求:
1.要有一个本地缓存管理器,管理着所有的缓存
2.本地缓存需要自动动态刷新,来保持和数据库数据的一致性
3.本地缓存要求不同系统能存储不同类型的数据
4.本地缓存要求能适配项目内所有的系统
第1点,增加一个缓存管理器非常的简单,就增加一个LocalCacheManager来管理所有的缓存即可。
第2点,可以使用线程来刷新。每隔一段时间,请求数据库,同步数据。
第3点,对于存储所有类型的数据,简单啊,泛型。
第4点,要求能适配。所以说设计模式还是要好好学一下的。我们可以用模板方法模式,我给你规定死了抽象方法,你就按照自己的需求实现就行了,其它的你不用管,我提供模板方法。
好了,不纸上谈兵了,看看具体的实现。
3.实现
先看抽象的本地缓存类,注释非常详细
/**
* 抽象缓存类
*/
public abstract class AbstractCache<V> {
//用于存储数据
private V data;
//在构造方法中,就把该缓存交给缓存管理器管理
public AbstractCache(String cacheName){
LocalCacheManage.add(cacheName,this);
}
//模板方法,final 不能改
protected final V get(){
if(null == this.data){
this.data = load();
}
return this.data;
}
//模板方法
protected final void refresh(){
this.data = load();
}
//各子系统自行实现该抽象方法
protected abstract V load();
}
在看看缓存管理器
/**
* 缓存管理器
*/
public class LocalCacheManage {
//可以看作是缓存池的概念,存储了本系统所有的缓存
private static Map<String, AbstractCache> caches = new HashMap<>();
static {
Thread refreshThread = new Thread(new RefreshThread());
refreshThread.setName("cache_refresh_thread");
//refreshThread.setDaemon(true);
// 在这里不设为守护进程,因为main方法执行完了,如果剩下的所有线程都是守护线程,那么jvm会直接退出!
// 但是在web服务中可以设置为守护线程
refreshThread.start();
}
//把缓存添加到缓存池
public static void add(String cacheName, AbstractCache abstractCache) {
caches.put(cacheName,abstractCache);
}
//获取缓存池
public static Map<String, AbstractCache> get(){
return caches;
}
//线程动态刷新
static class RefreshThread implements Runnable{
@Override
public void run() {
while (true){
try {
//实验起见,设置成1S,实际项目中至少1分钟以上刷新一次,否则数据库压力暴增,导致服务挂掉
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//遍历刷新缓存池中的所有缓存
if(null != caches && caches.size() > 0){
for(Map.Entry<String, AbstractCache> cacheEntry : caches.entrySet()){
cacheEntry.getValue().refresh();//这就是模板方法的好处
}
}
}
}
}
}
main方法
public class Main {
public static void main(String[] args) {
//==================第一个缓存==================
AbstractCache<List<String>> conf1 = new AbstractCache<List<String>>("confCache1") {
@Override
protected List<String> load() {
List<String> result = new ArrayList<String>();
//模拟从数据库获取数据,实际开发中可以进行数据库操作
System.out.println("我从数据库获得最新数据啦1");
result.add("conf1");
result.add("conf2");
return result;
}
};
//查看缓存中的数据
AbstractCache confCache1 = LocalCacheManage.get().get("confCache1");
List<String> list1 = (List<String>) confCache1.get();
System.out.println(list1.get(0));
//========================另一个缓存===============
AbstractCache<Map<String, Object>> conf2 = new AbstractCache<Map<String, Object>>("confCache2") {
@Override
protected Map<String, Object> load() {
Map<String, Object> map = new HashMap<>();
//模拟从数据库获取数据,实际开发中可以进行数据库操作
System.out.println("我从数据库获得最新数据啦2");
map.put("conf3","conf3333");
map.put("conf4","conf4444");
return map;
}
};
AbstractCache confCache2 = LocalCacheManage.get().get("confCache2");
Map<String, Object> map = (Map<String, Object>) confCache2.get();
System.out.println(map.get("conf4"));
}
}
//输出
我从数据库获得最新数据啦1
conf1
我从数据库获得最新数据啦2
conf4444
我从数据库获得最新数据啦1
我从数据库获得最新数据啦2
我从数据库获得最新数据啦1
我从数据库获得最新数据啦2
我从数据库获得最新数据啦1
我从数据库获得最新数据啦2
我从数据库获得最新数据啦1
我从数据库获得最新数据啦2
我从数据库获得最新数据啦1
我从数据库获得最新数据啦2
我从数据库获得最新数据啦1
我从数据库获得最新数据啦2
。。。。。一直执行下去,除非手动关闭jvm
4.分析
上面的代码注释都非常清晰,下面就讲解一些设计的精妙之处。
首先看AbstractCache,它使用了泛型,这样,放什么类型的都可以,Map,List,甚至是一个单一Object也可以,灵活。
再看AbstractCache的 load() 方法,这是一个抽象方法,它的作用就是为了获得数据,但是每个系统要的数据不一样啊,怎么办,声明成抽象的就行了,让子系统自己去实现。最巧妙的就是再它的两个模板方法,利用了load()方法。做到了 “你做好你的事,我做好我的事”。
再看AbstractCache的构造方法,直接把自己添加到了本地缓存管理器,只要你new出对象,就得给我进来,我就负责你的生命周期。
再看 LocalCacheManager,这个类有一个缓存池,就是用来存放所有的本地缓存的,就可以来管理这些缓存。并提供一个获取所有缓存的方法get()。最主要的是它的内置线程,自动刷新。遍历缓存池,对每一个缓存进行刷新,如何刷新? 还是要靠模板方法模式,直接由模板方法提供,进行统一式刷新。
5.总结
至此,我们实现了达到上述4点要求的缓存,是不是有点像SpringCluod的动态配置刷新,哈哈。虽然不知道大佬是不是从源码中学的,但是我是从大佬的源码中学的!虽然不知道这个有没有更好的方案,但是这个方案已经让我学习到了很多。