先说结论:本地缓存优先选用caffeine,因为性能比guava cache快,api风格与之兼容、能轻松地平滑迁移,并且在spring/spring boot最新版本中已经是默认本地缓存了。下面展开讲讲本地缓存和Spring cache。
本文讨论堆内缓存,暂不讨论堆外缓存。堆内缓存是指缓存与应用程序在一个JVM应用中,会受GC影响,一般业务层面的应用开发用不到堆外缓存。
1、什么场景使用本地缓存
并非所有的缓存场景,redis都适用,以下情况应当优先考虑本地缓存。
- 数据量不大
- 修改频率低、甚至是静态的数据
- 查询qps极高:通过纯内存操作,避免网络请求
- 对性能有极致要求,速度比redis更快
如:秒杀热点商品缓存、地域信息缓存。
2、缓存基本原理
先简单回顾一下缓存的基本原理。
语义
- get:根据key查询value,缓存未命中时查询底层数据并设置缓存
- put:设置缓存
- evict:删除key
- ttl:kv键值对过期失效
淘汰策略
缓存有大小上限,超过后需要淘汰掉冷数据,保留真正的热数据,以确保缓存命中率。有2种常见算法:LRU和LFU。
LRU(Least Recently Used)
优先淘汰掉最近最少使用的数据,该算法假设最近访问的数据,将来也会频繁地使用。
- 优点
- 容易理解和实现
- 链表占用空间小
- 缺点
- 临时批量数据,会把真正的热数据冲掉,而造成缓存命中率急剧下降,影响性能
下面给出LRU算法的2种伪代码实现:
- LinkedHashMap实现:LinkedHashMap底层数据结构就是一个HashMap和双向链表的结合体,可以借助它快速实现LRU算法。
public class LRUCache extends LinkedHashMap {
// 缓存最大条目数
private int maxSize;
public LRUCache(int maxSize) {
// 初始大小为16,loadFactor为0.75,accessOrder按访问顺序排列为true
super(16, 0.75f, true);
this.maxSize = maxSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
// 大小超过maxSize时,删除最久远的数据
return size() > maxSize;
}
}
- Map+Deque实现:
- Deque双端队列队尾是最新数据,队首是最久远数据
- 最新访问的元素,如果已存在,则移动至队尾,否则直接插入队尾
- put时如果已满,则删除队首元素
// 泛型支持
public class LRUCache<K, V> {
private int maxSize;
private Map<K, V> map = new HashMap<>();
// 双端队列,用于记录访问的远近;LinkedList是Deque的子类
private LinkedList<K> recencyList = new LinkedList<>();
public LRUCache(int maxSize) {
this.maxSize = maxSize;
}
public V get(K key) {
if (map.containsKey(key)) {
moveToTail(key);
return map.get(key);