1.原始状态:查库
(1)原始开发中,没有使用缓存机制(Ehcache,Redis)时,直接查库
(2)在用户量量不大的时候,查数据库或者读取文件是最为方便,能完全满足我们的业务要求
2.基于直接查库的优化JVM内置缓存:HashMap
(1)有一定用户量之后或者查询数据库特别频繁
(2)就可以使用的java中自带的HashMap或者ConcurrentHashMap
代码演示如下:
public class CuatomerService{
private HashMap<String.String> hashMap = new HashMap<>();
private CuatomerMapper customerMapper;
public String getCustomer(String name){
String customer = hashMap.get(name);
if(customer ==null){
customer = customerMapper.get(name);
hashMap.put(name,customer )
}
reruen customer;
}
}
<1>这样做就有个问题HashMap无法进行数据淘汰
<2>内存会无限制的增长,所以hashMap很快也被淘汰了
<3>hashMap可以在某些场景下作为缓存:
1.不需要淘汰机制的时候,利用反射
2.如果每次都通过反射去搜索Method,field,性能必定低效,这时我们用HashMap将其缓存起来,性能能提升很多
3.基于HashMap的优化:LRUHashMap
(1)HashMap问题在于无法进行数据淘汰,这样会导致内存无限膨胀
(2)引入淘汰算法,解决无法进行数据淘汰的问题
(3)常见的三种FIFO,LRU,LFU(还有一些ARC,MRU)
<1>FIFO:先进先出
1.先进入缓存的会先被淘汰
2.但是会导致命中率很低
3.会把首个数据但是他的访问频率很高给挤出
<2>LRU:最近最少使用算法
1.每次访问数据都会将其放在队尾
2.需要淘汰数据,就只需要淘汰队首即可
3.会把热点数据被淘汰
<3>LFU:最近最少频率使用
1.利用额外的空间记录每个数据的使用频率
2.选出频率最低进行淘汰
3.避免了LRU不能处理时间段的问题
(4)对于这三种,实现成本是一个比一个高,同样的命中率也是一个比一个好
(5)一般来说选择的方案居中即可,即实现成本不是太高,而命中率也还行的LRU
实现一个LRUMap的代码演示:
<1>可以通过继承LinkedHashMap
<2>重写removeEldestEntry方法,即可完成一个简单的LRUMap
public class LRUMap extends LinkedHashMap{
private final int max;
private object lock;
public LRUMap (int max.Object lock){
//无序扩容
super((int) (max*1.4f),0.75f,true);
this.max=max;
this.lock=lock;
}
/**
*(1)重写LinkedHashMap的removeEldestEntry方法即可
*(2)在Put的时候判断,如果为true,就会删除最老的
*/
@Override
protected boolean removeEldestEntry(Map.Entry eldest){
return size()>max;
}
public Object getValue(Object key){
synchronized(lock){
return get(key);
}
}
public Object putValue(Object key,Object value){
synchronized(lock){
put(key,value);
}
public boolean removeValue(Object key){
synchronized(lock){
return remove(key) != null;
}
}
public boolean removeAll(){
clear();
return true;
}
}
<3>在LinkedHashMap中维护了一个entry(用来放key和value的对象)链表
<4>每一次get或者put的时候都会把插入的新entry,或查询到的老entry放在链表末尾
<5>在构造方法中,设置的大小特意设置到max*1.4,在下面的removeEldestEntry方法中只需要size>max就淘汰
<6>这个map永远也走不到扩容的逻辑了,通过重写LinkedHashMap,几个简单的方法实现了LruMap
(6)LRUMap用来进行缓存数据的淘汰,但是有几个问题:
<1>锁竞争严重:Lock是全局锁,在方法级别上面的,当调用量较大时,性能必然会比较低
<2>不支持过期时间
<3>不支持自动刷新
4.基于LRUHashMap的优化:Guava cache
(1)对于LRUHashMapd1这些问题,发明了Guava cache
(2)Guava cache代码演示如下:
(3)将会从guava cache原理中,解释guava cache是如何解决LRUMap的几个问题的
<1>锁竞争
1.guava cache采用了类似ConcurrentHashMap的思想,分段加锁,在每个段里面各自负责自己的淘汰的事情
2.段太少那竞争依然很严重,如果段太多会容易出现随机淘汰
3.在guava cache中通过代码,计算出应该如何分段
4.在guava cache中对于写操作直接加锁
5.对于读操作,如果读取的数据没有过期,且已经加载就绪,不需要进行加锁
6.如果没有读到会再次加锁进行二次读
7.如果还没有需要进行缓存加载,也就是通过配置的CacheLoader,这里配置的是直接返回Key,在业务中通常配置从数据库中查询
<2>过期时间
1.相比于LRUMap多了两种过期时间,一个是写后多久过期expireAfterWrite,一个是读后多久过期expireAfterAccess
<3>自动刷新
1.直接通过查询,判断其是否满足刷新条件,进行刷新
<4>其他特性
1.虚引用
2.在Guava cache中,key和value都能进行虚引用的设定
3.用来记录被回收的引用,其中每个队列记录了每个被回收的Entry的hash
4.回收了之后通过这个队列中的hash值就能把以前的Entry进行删除
5.删除监听器
6.在guava cache中,当有数据被淘汰时,但是不知道他到底是过期,还是被驱逐,还是因为虚引用的对象被回收
7.可以调用这个方法removalListener(RemovalListener listener)添加监听器进行数据淘汰的监听,可以打日志或者一些其他处理,可以用来进行数据淘汰分析
8.在RemovalCause记录了所有被淘汰的原因:被用户删除,被用户替代,过期,驱逐收集,由于大小淘汰
(4)guava cache的总结
<1>是一个性能不错的,api丰富的LRU Map
<2>通过对guava cache的二次开发,让其可以进行java应用服务之间的缓存更新
5.基于Guava cache的优化:W-TinyLFU(LFU+LRU算法的变种)
(1)guava cache的功能的确是很强大,满足了绝大多数的人的需求,但是其本质上还是LRU的一层封装
(2)命中率W-TinyLFU 优于了guava cache,是最接近理想命中率的
(3)读写吞吐量上面也是完爆guava cache
(4)在W-TinyLFU,对这些事件的操作是通过异步操作,他将事件提交至队列,这里的队列的数据结构是RingBuffer
(5)在W-TinyLFU中认为缓存读比写多很多,所以对于写操作是所有线程共享一个Ringbuffer
(6)W-TinyLFU所有的数据都在ConcurrentHashMap中
(7)W-TinyLFU的api借鉴了Guava的api,可以发现其基本一模一样
6.上面5种本地缓存不适于与分布式、微服务架构
7.本地缓存Ehcache可以支持分布式、微服务架构,弥补了上面5种的不足