Guava 源码分析(Cache 原理)

1.jpeg

前言

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

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

缓存

本次主要讨论缓存。github

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

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

但也不是什么好处都占,读取速度快了可是它的内存更小资源更宝贵,因此咱们应当缓存真正须要的数据。安全

其实也就是典型的空间换时间。数据结构

下面谈谈 Java 中所用到的缓存。并发

<!--more-->分布式

JVM 缓存

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

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

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

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

Ehcache、Guava Cache

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

它具备上文 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.png

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

2.png

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

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

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

接着往下跟到:

3.png

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

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

5.png

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

6.png

到了这里也很清晰了:

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

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

应该是如下缘由:

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

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

总结

最后再来总结下 Guava 的 Cache。

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

7.png

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

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

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

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

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

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

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

最后插播个小广告:

Java-Interview 截止目前将近 8K star。

此次定个小目标:争取冲击 1W star

感谢各位老铁的支持与点赞。

欢迎关注公众号一块儿交流:

相关文章
相关标签/搜索