Guava 源码分析(Cache 原理)

简介: Google 出的 Guava 是 Java 核心增强的库,应用非常广泛。我平时用的也挺频繁,这次就借助日常使用的 Cache 组件来看看 Google 大牛们是如何设计的。

缓存


本次主要讨论缓存。


缓存在日常开发中举足轻重,如果你的应用对某类数据有着较高的读取频次,并且改动较小时那就非常适合利用缓存来提高性能。


缓存之所以可以提高性能是因为它的读取效率很高,就像是 CPU 的 L1、L2、L3 缓存一样,级别越高相应的读取速度也会越快。


但也不是什么好处都占,读取速度快了但是它的内存更小资源更宝贵,所以我们应当缓存真正需要的数据。


其实也就是典型的空间换时间。


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


JVM 缓存


首先是 JVM 缓存,也可以认为是堆缓存。


其实就是创建一些全局变量,如 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 到底是怎么实现。


原理分析


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


示例代码在这里:


github.com/crossoverJi…



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



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


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


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


接着往下跟到:



2761 行,根据方法名称可以看出是判断当前的 Entry 是否过期,该 entry 就是通过 key 查询到的。



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



如果过期就往下走,尝试进行过期删除(需要加锁,后面会具体讨论)。



到了这里也很清晰了:


  • 获取当前缓存的总数量


  • 自减一(前面获取了锁,所以线程安全)


  • 删除并将更新的总数赋值到 count。


其实大体上就是这个流程,Guava 并没有按照之前猜想的另起一个线程来维护过期数据。


应该是以下原因:


  • 新起线程需要资源消耗。


  • 维护过期数据还要获取额外的锁,增加了消耗。


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


总结


最后再来总结下 Guava 的 Cache。


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



如果有看过 ConcurrentHashMap 的原理 应该会想到这其实非常类似。


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


先找到 Segment,再找具体的位置,等于是做了两次 Hash 定位。


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


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


因为作为一个给开发者使用的工具,需要有很多的自定义属性,利用构建则模式再合适不过了。


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


相关文章
|
缓存 NoSQL Java
Java工具篇之Guava-cache内存缓存
常在业务系统中做开发,不会点高级知识点,有点不好意思了。在业务系统中,提高系统响应速度,提供系统高并发能力,其实方向很简单,三个方向,六个字而已: **缓存降级限流。** 当然这是在排除代码质量非常差的情况,如果代码质量很差,都是while循环和高内存占用,那么其实再怎么做都于事无补。除非你有一个马云爸爸,性能不够,机器来凑嘛。阿里云前来支持(1000台机器够了吗?)
1287 0
|
4月前
|
缓存 Java
Java本地高性能缓存实践问题之使用Caffeine的Cache接口来查找一个缓存元素的问题如何解决
Java本地高性能缓存实践问题之使用Caffeine的Cache接口来查找一个缓存元素的问题如何解决
|
4月前
|
存储 缓存 监控
Java本地高性能缓存实践问题之Guava Cache被Caffeine所取代的问题如何解决
Java本地高性能缓存实践问题之Guava Cache被Caffeine所取代的问题如何解决
|
存储 设计模式 缓存
Java源码分析:Guava之不可变集合ImmutableMap的源码分析
Java源码分析:Guava之不可变集合ImmutableMap的源码分析
66 0
|
7月前
|
存储 缓存 Java
java如何实现一个LRU(最近最少使用)缓存?
实现了一个LRU缓存,使用双向链表保持访问顺序,哈希表用于定位元素。Java代码中,`LRUCache`类包含容量、哈希表及链表头尾节点。`get`方法查哈希表,找到则移动至链表头并返回值,否则返回-1。`put`方法更新或插入节点,超出容量则移除最不常用节点。
63 6
|
7月前
|
缓存 NoSQL Java
Guava Cache 异步刷新技巧,你值得拥有!
Guava Cache是一款非常优秀的本地缓存框架,提供非常简洁易用的 API 供开发者使用。 这篇文章,我们聊聊如何使用 Guava Cache **异步刷新技巧**带飞系统性能 。
Guava Cache 异步刷新技巧,你值得拥有!
|
7月前
|
存储 缓存 NoSQL
Guava 缓存详解及使用
Guava Cache 是`Google Fuava`中的一个内存缓存模块,用于将数据缓存到JVM内存中。 本文主要介绍下Guava缓存的配置详解及相关使用 缓存分为本地缓存与分布式缓存。本地缓存为了保证线程安全问题,一般使用`ConcurrentMap`的方式保存在内存之中,而常见的分布式缓存则有`Redis`,`MongoDB`等。
|
存储 缓存 监控
【深入浅出Spring原理及实战】「缓存Cache开发系列」带你深入分析Spring所提供的缓存Cache抽象详解的核心原理探索
缓存的工作机制是先从缓存中读取数据,如果没有再从慢速设备上读取实际数据,并将数据存入缓存中。通常情况下,我们会将那些经常读取且不经常修改的数据或昂贵(CPU/IO)的且对于相同请求有相同计算结果的数据存储到缓存中。
197 1
|
存储 缓存 NoSQL
Java Cache 缓存方案详解及代码-Ehcache
Java Cache 缓存方案详解及代码-Ehcache
612 0
|
缓存 Java
Guava 源码分析(Cache 原理【二阶段】)(下)
在上文「Guava 源码分析(Cache 原理)」中分析了 Guava Cache 的相关原理。 文末提到了回收机制、移除时间通知等内容,许多朋友也挺感兴趣,这次就这两个内容再来分析分析。 在开始之前先补习下 Java 自带的两个特性,Guava 中都有具体的应用。