缓存的含义
缓存就是内存中的一个对象,可以基于此对象存储其它对象
缓存的作用
降低数据库的访问压力,提高数据的查询效率,改善用户体验
缓存的分类
- 数据库内置的缓存?(例如 mysql 的查询缓存)
- 数据层缓存(一般由持久层框架提供,例如 MyBatis)
- 业务层缓存(基于 map 等实现的本缓存,分布式缓存-例如 redis)
- 浏览器内置缓存?
- CPU 缓存(高速缓冲区)
缓存设计要考虑哪些问题?
- 存储结构(使用什么结构存储数据效率会更高?-散列表)
- 淘汰策略(缓存容量有限-LRU/FIFO/LFU,软应用、弱引用)
- 任务调度(定期刷新缓存,缓存失效时间)
- 并发安全(缓存并发访问时的线程安全)
- 日志记录(缓存是否命中,命中率是多少)
- 序列化(存对象时序列化、取对象时反序列化
缓存接口设计
public interface Cache {
void putObject(Object key,Object value);
Object getObject(Object key);
Object removeObject(Object key);
void clear();
int size();
}
1.基于HashMap实现简易缓存
public class PerpetualCache implements Cache{
private HashMap<Object,Object> cache=new HashMap<>();
@Override
public void putObject(Object key, Object value) {
cache.put(key,value);
}
@Override
public Object getObject(Object key) {
return cache.get(key);
}
...
}
2.基于Deque实现Fifo淘汰算法的缓存
public class FifoCache implements Cache{
/**关联Cache对象-找到可以存储数据的基层对象*/
private Cache cache;
/**定义Cache的最大容量*/
private int maxCap;
/**通过队列记录key的添加顺序,Deque是JAVA中的双端队列,用于存储对应的key*/
private Deque<Object> keyOrders;
public FifoCache(Cache cache, int maxCap) {
this.cache = cache;
this.maxCap = maxCap;
this.keyOrders=new LinkedList<>();
}
@Override
public void putObject(Object key, Object value) {
//1.记录key的顺序(通过添加在队列的尾部实现)
keyOrders.addLast(key);
//2.判断容器是否已满,满了则移除
if (keyOrders.size()>maxCap){
Object firstKey = keyOrders.removeFirst();
//从cache中清除指定key对应的对象
cache.removeObject(firstKey);
}
//3.向cache添加数据
cache.putObject(key,value);
}
@Override
public Object getObject(Object key) {
return cache.getObject(key);
}
@Override
public Object removeObject(Object key) {
Object object =cache.removeObject(key);
keyOrders.remove(key);
return object;
}
...
}
3.基于LinkedHashMap实现LRU淘汰算法的缓存
/**关联Cache对象-找到可以存储数据的基层对象*/
private Cache cache;
/**定义Cache的最大容量*/
private int maxCap;
/**通过LinkedHashMap记录key的访问顺序*/
private LinkedHashMap<Object,Object> keyAccessOrders;
/**记录最近访问次数最少的key*/
private Object eldEstKey;
public LruCache(Cache cache, int maxCap) {
this.cache = cache;
this.maxCap = maxCap;
keyAccessOrders=new LinkedHashMap<Object,Object>(maxCap,0.75f,true){
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
boolean isFull= size()>maxCap;
if(isFull)
eldEstKey=eldest.getKey();//获取到最近访问次数最少的key
return isFull;
}
};
}
@Override
public void putObject(Object key, Object value) {
cache.putObject(key,value);
keyAccessOrders.put(key,key);//2.记录key的访问顺序
if (eldEstKey!=null){
cache.removeObject(eldEstKey);//淘汰最近访问最少的
eldEstKey=null;
}
}
@Override
public Object getObject(Object key) {
//1.记录key的访问顺序
keyAccessOrders.get(key);
//2.返回cache中的指定key对应的value
return cache.getObject(key);
}
@Override
public Object removeObject(Object key) {
Object object = cache.removeObject(key);
keyAccessOrders.remove(key);
return object;
}
...
}
FAQ:
1.Fifo和LRU算法淘汰策略的区别?
First in First out 先进先出, 判断被存储的时间,离目前最远的数据优先被淘汰, 针对于内存中访问频率比较高,但是放入的时间又比较早的对象而言,命中率会比较低
Least Recently Used,最近最少使用。判断最近被使用的时间,目前最远的数据优先被淘汰. LRU算法存在着“缓存污染”的情况需要避免,当突然有一批非热点元素查询打入,大量的非热点数据就会被加载到缓存队列中,从而把真正的热点元素给“挤出去”
2.LFU算法拓展
Least Frequently Used,最不经常使用。在一段时间内,数据被使用次数最少的,优先被淘汰 .需要耗费额外的空间来存储每个元素的访问频率,因此随着缓存元素的数目不断增大,计数器的个数也在不断地增大