Guava 源码分析之Cache的实现原理

Guava 源码分析之Cache的实现原理

前言

Google 出的 Guava 是 Java 核心加强的库,应用很是普遍。java

我平时用的也挺频繁,此次就借助平常使用的 Cache 组件来看看 Google 大牛们是如何设计的。git

缓存

本次主要讨论缓存。缓存在平常开发中举足轻重,若是你的应用对某类数据有着较高的读取频次,而且改动较小时那就很是适合利用缓存来提升性能。github

缓存之因此能够提升性能是由于它的读取效率很高,就像是 CPU 的 L一、L二、L3 缓存同样,级别越高相应的读取速度也会越快。算法

但也不是什么好处都占,读取速度快了可是它的内存更小资源更宝贵,因此咱们应当缓存真正须要的数据。其实也就是典型的空间换时间。下面谈谈 Java 中所用到的缓存。缓存

 

JVM 缓存

首先是 JVM 缓存,也能够认为是堆缓存。安全

其实就是建立一些全局变量,如 Map、List 之类的容器用于存放数据。数据结构

这样的优点是使用简单可是也有如下问题:并发

  • 只能显式的写入,清除数据。
  • 不能按照必定的规则淘汰数据,如 LRU,LFU,FIFO 等。
  • 清除数据时的回调通知。
  • 其余一些定制功能等。

Ehcache、Guava Cache

因此出现了一些专门用做 JVM 缓存的开源工具出现了,如本文提到的 Guava Cache。jvm

它具备上文 JVM 缓存不具备的功能,如自动清除数据、多种清除算法、清除回调等。分布式

但也正由于有了这些功能,这样的缓存必然会多出许多东西须要额外维护,天然也就增长了系统的消耗。

分布式缓存

刚才提到的两种缓存其实都是堆内缓存,只能在单个节点中使用,这样在分布式场景下就招架不住了。

因而也有了一些缓存中间件,如 Redis、Memcached,在分布式环境下能够共享内存。

具体不在本次的讨论范围。

Guava Cache 示例

之因此想到 Guava 的 Cache,也是最近在作一个需求,大致以下:

从 Kafka 实时读取出应用系统的日志信息,该日志信息包含了应用的健康情况。
若是在时间窗口 N 内发生了 X 次异常信息,相应的我就须要做出反馈(报警、记录日志等)。

对此 Guava 的 Cache 就很是适合,我利用了它的 N 个时间内不写入数据时缓存就清空的特色,在每次读取数据时判断异常信息是否大于 X 便可。

伪代码以下:

@Value("${alert.in.time:2}") private int time ; @Bean public LoadingCache buildCache(){ return CacheBuilder.newBuilder() .expireAfterWrite(time, TimeUnit.MINUTES) .build(new CacheLoader<Long, AtomicLong>() { @Override public AtomicLong load(Long key) throws Exception { return new AtomicLong(0); } }); } /** * 判断是否须要报警 */ public void checkAlert() { try { if (counter.get(KEY).incrementAndGet() >= limit) { LOGGER.info("***********报警***********"); //将缓存清空 counter.get(KEY).getAndSet(0L); } } catch (ExecutionException e) { LOGGER.error("Exception", e); } } 

首先是构建了 LoadingCache 对象,在 N 分钟内不写入数据时就回收缓存(当经过 Key 获取不到缓存时,默认返回 0)。

而后在每次消费时候调用 checkAlert() 方法进行校验,这样就能够达到上文的需求。

咱们来设想下 Guava 它是如何实现过时自动清除数据,而且是能够按照 LRU 这样的方式清除的。

大胆假设下:

内部经过一个队列来维护缓存的顺序,每次访问过的数据移动到队列头部,而且额外开启一个线程来判断数据是否过时,过时就删掉。有点相似于我以前写过的 动手实现一个 LRU cache

胡适说过:大胆假设当心论证

下面来看看 Guava 究竟是怎么实现。

原理分析

看原理最好不过是跟代码一步步走了:

示例代码在这里:

https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/guava/CacheLoaderTest.java

8.png8.png

为了能看出 Guava 是怎么删除过时数据的在获取缓存以前休眠了 5 秒钟,达到了超时条件。

2.png2.png

最终会发如今 com.google.common.cache.LocalCache 类的 2187 行比较关键。

再跟进去以前第 2182 行会发现先要判断 count 是否大于 0,这个 count 保存的是当前缓存的数量,并用 volatile 修饰保证了可见性。

更多关于 volatile 的相关信息能够查看 你应该知道的 volatile 关键字

接着往下跟到:

3.png3.png

2761 行,根据方法名称能够看出是判断当前的 Entry 是否过时,该 entry 就是经过 key 查询到的。

4.png4.png

这里就很明显的看出是根据根据构建时指定的过时方式来判断当前 key 是否过时了。

5.png5.png

若是过时就往下走,尝试进行过时删除(须要加锁,后面会具体讨论)。

6.png6.png

到了这里也很清晰了:

  • 获取当前缓存的总数量
  • 自减一(前面获取了锁,因此线程安全)
  • 删除并将更新的总数赋值到 count。

其实大致上就是这个流程,Guava 并无按照以前猜测的另起一个线程来维护过时数据。

应该是如下缘由:

  • 新起线程须要资源消耗。
  • 维护过时数据还要获取额外的锁,增长了消耗。

而在查询时候顺带作了这些事情,可是若是该缓存迟迟没有访问也会存在数据不能被回收的状况,不过这对于一个高吞吐的应用来讲也不是问题。

总结

最后再来总结下 Guava 的 Cache。

其实在上文跟代码时会发现经过一个 key 定位数据时有如下代码:

7.png7.png

若是有看过 ConcurrentHashMap 的原理 应该会想到这其实很是相似。

其实 Guava Cache 为了知足并发场景的使用,核心的数据结构就是按照 ConcurrentHashMap 来的,这里也是一个 key 定位到一个具体位置的过程。

先找到 Segment,再找具体的位置,等因而作了两次 Hash 定位。

上文有一个假设是对的,它内部会维护两个队列 accessQueue,writeQueue用于记录缓存顺序,这样才能够按照顺序淘汰数据(相似于利用 LinkedHashMap 来作 LRU 缓存)。

同时从上文的构建方式来看,它也是构建者模式来建立对象的。

由于做为一个给开发者使用的工具,须要有不少的自定义属性,利用构建则模式再合适不过了。

Guava 其实还有不少东西没谈到,好比它利用 GC 来回收内存,移除数据时的回调通知等。以后再接着讨论。

参考:Guava 源码分析之Cache的实现原理

相关文章
相关标签/搜索