--- title: Java 进程内缓存 date: 2022-02-17 22:34:30 order: 05 categories: - Java - 中间件 - 缓存 tags: - Java - 中间件 - 缓存 permalink: /pages/45a5db60/ --- # Java 进程内缓存 > 关键词:ConcurrentHashMap、LRUHashMap、Guava Cache、Caffeine、Ehcache ## 一、ConcurrentHashMap 最简单的进程内缓存可以通过 JDK 自带的 `HashMap` 或 `ConcurrentHashMap` 实现。 适用场景:**不需要淘汰的缓存数据**。 缺点:无法进行缓存淘汰,内存会无限制的增长。 ## 二、LRUHashMap 可以通过**继承 `LinkedHashMap` 来实现一个简单的 `LRUHashMap`**,即可完成一个简单的 **LRU (最近最少使用)**算法。 缺点: - 锁竞争严重,性能比较低。 - 不支持过期时间 - 不支持自动刷新 【示例】LRUHashMap 的简单实现 ```java 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](https://github.com/ben-manes/caffeine) 是一个使用 JDK8 改进 Guava 缓存的高性能缓存库。 Caffeine 实现了 W-TinyLFU(**LFU** + **LRU** 算法的变种),其**命中率和读写吞吐量大大优于 Guava Cache**。 其实现原理较复杂,可以参考[你应该知道的缓存进化史](https://juejin.im/post/5b7593496fb9a009b62904fa#comment)。 ## 五、Ehcache > 参考:[Ehcache](04.Ehcache.md) ## 六、进程内缓存对比 常用进程内缓存技术对比: | 比较项 | 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`**。 ## 参考资料 - [caffeine github](https://github.com/ben-manes/caffeine) - [深入解密来自未来的缓存-Caffeine](https://juejin.im/post/5b8df63c6fb9a019e04ebaf4) - [Caffeine 缓存](https://www.jianshu.com/p/9a80c662dac4) - [Google Guava 官方教程(中文版)](https://wizardforcel.gitbooks.io/guava-tutorial/content/1.html) - [Google Guava Cache 全解析](https://www.jianshu.com/p/38bd5f1cf2f2) - [注释驱动的 Spring cache 缓存介绍](https://developer.ibm.com/zh/articles/os-cn-spring-cache/)