• Guava 源码分析(Cache 原理)


     

    crossoverJie 程序猿DD 昨天

    作者:crossoverJie’s Blog

    来源:https://crossoverjie.top/2018/06/13/guava/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 即可。

    伪代码如下:

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    25

    26

    27

    28

    29

    30

    31

    32

    @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

    为了能看出 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 来回收内存,移除数据时的回调通知等。之后再接着讨论。

    - END -

     往期推荐:

    • 死磕Java系列:

    1. 深入分析ThreadLocal

    2. 深入分析synchronized的实现原理

    3. 深入分析volatile的实现原理

    4. Java内存模型之happens-before

    5. Java内存模型之重排序

    6. Java内存模型之分析volatile

    7.  

      Java内存模型之总结

    8. J.U.C之AQS简介

    9. J.U.C之AQS:CLH同步队列

    10. J.U.C之AQS同步状态的获取与释放

    11. J.U.C之AQS阻塞和唤醒线程

    12. J.U.C之重入锁:ReentrantLock

    13. J.U.C之读写锁:ReentrantReadWriteLock

    14. J.U.C之Condition

    15. J.U.C之并发工具类:CyclicBarrier

    16. J.U.C之并发工具类:Semaphore

    17. J.U.C之并发工具类:CountDownLatch

    ……

    • Spring系列:

    1. Spring Cloud Zuul中使用Swagger汇总API接口文档

    2. Spring Cloud Config Server迁移节点或容器化带来的问题

    3. Spring Cloud Config对特殊字符加密的处理

    4. Spring Boot使用@Async实现异步调用:使用Future以及定义超时

    5. Spring Cloud构建微服务架构:分布式配置中心(加密解密)

    6. Spring Boot快速开发利器:Spring Boot CLI

    ……

    https://mp.weixin.qq.com/s?__biz=MzAxODcyNjEzNQ==&mid=2247485483&idx=2&sn=816bbf4f92958282023410170d145ae2&chksm=9bd0a5b3aca72ca50ac19943dd8a11faeee65b3b8b2401871a07fb55d741b3230c84f6d9db9f&scene=0&key=2c53aeb2caef3bd54e1f683f3aaa00132821c235e3c682a1b4d532a94b414f0e49e08df973ad3979437530e8325b9b38fd81cdf8c6dc488bf60dc7f80e5e671d12f7e110587ecbd1d01c329d7be4e84d&ascene=1&uin=MjgwMTEwNDQxNg%3D%3D&devicetype=Windows-QQBrowser&version=6103000b&lang=zh_CN&pass_ticket=vvKcLn3PyYHeCl52ezzWzhSztycb4H81TQpDL6JrUh8nOWpLaKAU5p7c7No6kFAC
  • 相关阅读:
    pycharm下同存Python2和Python3的运行方式问题
    Python3安装requests库
    jmeter实现接口传参为一个文件的测试场景
    解决jmeter添加HTTP Cookie Manager后,还是cookie错误的问题
    jmeter添加webSocket请求
    sublime集成Python环境
    jmeter实现排除登录对单个事务的压测
    JavaScript语法
    JavaScript数据
    javaScript基础
  • 原文地址:https://www.cnblogs.com/bigben0123/p/9293143.html
Copyright © 2020-2023  润新知