Java 进程内缓存
# Java 进程内缓存
关键词:ConcurrentHashMap、LRUHashMap、Guava Cache、Caffeine、Ehcache
# 一、ConcurrentHashMap
最简单的进程内缓存可以通过 JDK 自带的 HashMap
或 ConcurrentHashMap
实现。
适用场景:不需要淘汰的缓存数据。
缺点:无法进行缓存淘汰,内存会无限制的增长。
# 二、LRUHashMap
可以通过继承 LinkedHashMap
来实现一个简单的 LRUHashMap
,即可完成一个简单的 **LRU (最近最少使用)**算法。
缺点:
- 锁竞争严重,性能比较低。
- 不支持过期时间
- 不支持自动刷新
【示例】LRUHashMap 的简单实现
class LRUCache extends LinkedHashMap {
private final int max;
private Object lock;
public LRUCache(int max) {
//无需扩容
super((int) (max * 1.4f), 0.75f, true);
this.max = max;
this.lock = new Object();
}
/**
* 重写LinkedHashMap的removeEldestEntry方法即可 在Put的时候判断,如果为true,就会删除最老的
*
* @param eldest
* @return
*/
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > max;
}
public Object getValue(Object key) {
synchronized (lock) {
return get(key);
}
}
public void 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;
}
}
# 三、Guava Cache
Guava Cache 解决了 LRUHashMap
中的几个缺点。
Guava Cache 提供了基于容量,时间和引用的缓存回收方式。基于容量的方式内部实现采用 LRU 算法,基于引用回收很好的利用了 Java 虚拟机的垃圾回收机制。
其中的缓存构造器 CacheBuilder 采用构建者模式提供了设置好各种参数的缓存对象。缓存核心类 LocalCache 里面的内部类 Segment 与 jdk1.7 及以前的 ConcurrentHashMap
非常相似,分段加锁,减少锁竞争,并且都继承于 ReetrantLock
,还有六个队列,以实现丰富的本地缓存方案。Guava Cache 对于过期的 Entry 并没有马上过期(也就是并没有后台线程一直在扫),而是通过进行读写操作的时候进行过期处理,这样做的好处是避免后台线程扫描的时候进行全局加锁。
直接通过查询,判断其是否满足刷新条件,进行刷新。
# Guava Cache 缓存回收
Guava Cache 提供了三种基本的缓存回收方式。
# 基于容量回收
maximumSize(long)
:当缓存中的元素数量超过指定值时触发回收。
# 基于定时回收
expireAfterAccess(long, TimeUnit)
:缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样。expireAfterWrite(long, TimeUnit)
:缓存项在给定时间内没有被写访问(创建或覆盖),则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。
如下文所讨论,定时回收周期性地在写操作中执行,偶尔在读操作中执行。
# 基于引用回收
CacheBuilder.weakKeys()
:使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。CacheBuilder.weakValues()
:使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收。CacheBuilder.softValues()
:使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。
# Guava Cache 核心 API
# CacheBuilder
缓存构建器。构建缓存的入口,指定缓存配置参数并初始化本地缓存。 主要采用 builder 的模式,CacheBuilder 的每一个方法都返回这个 CacheBuilder 知道 build 方法的调用。 注意 build 方法有重载,带有参数的为构建一个具有数据加载功能的缓存,不带参数的构建一个没有数据加载功能的缓存。
# LocalManualCache
作为 LocalCache 的一个内部类,在构造方法里面会把 LocalCache 类型的变量传入,并且调用方法时都直接或者间接调用 LocalCache 里面的方法。
# LocalLoadingCache
可以看到该类继承了 LocalManualCache 并实现接口 LoadingCache。 覆盖了 get,getUnchecked 等方法。
# LocalCache
Guava Cache 中的核心类,重点了解。
LocalCache 的数据结构与 ConcurrentHashMap 很相似,都由多个 segment 组成,且各 segment 相对独立,互不影响,所以能支持并行操作。每个 segment 由一个 table 和若干队列组成。缓存数据存储在 table 中,其类型为 AtomicReferenceArray。
# 四、Caffeine
caffeine (opens new window) 是一个使用 JDK8 改进 Guava 缓存的高性能缓存库。
Caffeine 实现了 W-TinyLFU(LFU + LRU 算法的变种),其命中率和读写吞吐量大大优于 Guava Cache。
其实现原理较复杂,可以参考你应该知道的缓存进化史 (opens new window)。
# 五、Ehcache
参考:Ehcache
# 六、进程内缓存对比
常用进程内缓存技术对比:
比较项 | ConcurrentHashMap | LRUMap | Ehcache | Guava Cache | Caffeine |
---|---|---|---|---|---|
读写性能 | 很好,分段锁 | 一般,全局加锁 | 好 | 好,需要做淘汰操作 | 很好 |
淘汰算法 | 无 | LRU,一般 | 支持多种淘汰算法,LRU,LFU,FIFO | LRU,一般 | W-TinyLFU, 很好 |
功能丰富程度 | 功能比较简单 | 功能比较单一 | 功能很丰富 | 功能很丰富,支持刷新和虚引用等 | 功能和 Guava Cache 类似 |
工具大小 | jdk 自带类,很小 | 基于 LinkedHashMap,较小 | 很大,最新版本 1.4MB | 是 Guava 工具类中的一个小部分,较小 | 一般,最新版本 644KB |
是否持久化 | 否 | 否 | 是 | 否 | 否 |
是否支持集群 | 否 | 否 | 是 | 否 | 否 |
ConcurrentHashMap
- 比较适合缓存比较固定不变的元素,且缓存的数量较小的。虽然从上面表格中比起来有点逊色,但是其由于是 JDK 自带的类,在各种框架中依然有大量的使用,比如我们可以用来缓存我们反射的 Method,Field 等等;也可以缓存一些链接,防止其重复建立。在 Caffeine 中也是使用的ConcurrentHashMap
来存储元素。LRUMap
- 如果不想引入第三方包,又想使用淘汰算法淘汰数据,可以使用这个。Ehcache
- 由于其 jar 包很大,较重量级。对于需要持久化和集群的一些功能的,可以选择 Ehcache。需要注意的是,虽然 Ehcache 也支持分布式缓存,但是由于其节点间通信方式为 rmi,表现不如 Redis,所以一般不建议用它来作为分布式缓存。Guava Cache
- Guava 这个 jar 包在很多 Java 应用程序中都有大量的引入,所以很多时候其实是直接用就好了,并且其本身是轻量级的而且功能较为丰富,在不了解 Caffeine 的情况下可以选择 Guava Cache。Caffeine
- 其在命中率,读写性能上都比 Guava Cache 好很多,并且其 API 和 Guava cache 基本一致,甚至会多一点。在真实环境中使用 Caffeine,取得过不错的效果。
总结一下:如果不需要淘汰算法则选择 ConcurrentHashMap
,如果需要淘汰算法和一些丰富的 API,推荐选择 Caffeine
。