• Guava Cache 异步刷新技巧,你值得拥有!


    Guava Cache是一款非常优秀的本地缓存框架,提供简洁易用的 API 供开发者使用。

    这篇文章,我们聊聊如何使用 Guava Cache 异步刷新技巧带飞系统性能 。

    1 基本用法

    首先,在 Java 应用中添加 maven 依赖:

    <dependency>
         <groupId>com.google.guavagroupId>
         <artifactId>guavaartifactId>
         <version>31.0.1-jreversion>
    dependency>
    

    然后编写测试用例:

    LoadingCache 是本地缓存工具,支持配置加载函数定时失效等功能。

    例子中配置了基于容量进行回收策略,缓存最大容量设置为 100,配置了定时失效刷新函数

    1. 定时失效

    配置 expireAfterWrite 后,缓存项在被创建或最后一次更新后的指定时间内会过期。

    1. 刷新函数

    配置 refreshAfterWrite 设置刷新时间,当缓存项过期的同时可以重新加载新值 。

    我们模拟过期执行 load 方法 / 重新加载执行 reload 方法的流程,执行结果见下图:

    执行结果表明:Guava Cache 并没有后台任务线程异步的执行 load 或者 reload 方法。

    1. expireAfterWrite 允许一个线程执行 load 方法,其他线程阻塞等待 。

      当大量线程用相同的 key 获取缓存值时,只会有一个线程进入 load 方法,而其他线程则等待,直到缓存值被生成。这样也就避免了缓存击穿的危险。高并发场景下 ,这样还是会阻塞大量线程。

    2. refreshAfterWrite 允许一个线程执行 load 方法,其他线程返回旧的值。

      单个 key 并发下,使用 refreshAfterWrite ,虽然不会阻塞了,但是如果恰巧同时多个 key 同时过期,还是会给数据库造成压力。

    为了提升系统性能,我们可以从如下两个方面来处理 :

    1. 减少过期的频率 ,也就是减少执行 load 方法的频率 ,配置 refresh < expire 。
    2. 采用异步刷新的策略,也就是线程异步加载数据,期间所有请求返回旧的缓存值

    2 两种方式实现异步刷新

    2.1 重写 reload 方法

    2.2 实现 asyncReloading 方法

    不管使用哪种方案, 都需要定义单独的线程池来执行刷新任务 。

    3 异步刷新 + 多级缓存

    2018 年,笔者服务的一家电商公司需要进行 app 首页接口的性能优化。笔者花了大概两天的时间完成了整个方案,采取的是两级缓存模式,同时采用了 Guava 的异步刷新机制。

    整体架构如下图所示:

    缓存读取流程如下 :

    1、业务网关刚启动时,本地缓存没有数据,读取 Redis 缓存,如果 Redis 缓存也没数据,则通过 RPC 调用导购服务读取数据,然后再将数据写入本地缓存和 Redis 中;若 Redis 缓存不为空,则将缓存数据写入本地缓存中。

    2、由于步骤1已经对本地缓存预热,后续请求直接读取本地缓存,返回给用户端。

    3、Guava 配置了 refresh 机制,每隔一段时间会调用自定义 LoadingCache 线程池(5个最大线程,5个核心线程)去导购服务同步数据到本地缓存和 Redis 中。

    优化后,性能表现很好,平均耗时在 5ms 左右,同时大幅度的减少应用 GC 的频率。

    该方案依然有瑕疵,一天晚上我们发现 app 端首页显示的数据时而相同,时而不同。

    也就是说: 虽然 LoadingCache 线程一直在调用接口更新缓存信息,但是各个服务器本地缓存中的数据并非完成一致。

    这说明了两个很重要的点:

    1、惰性加载仍然可能造成多台机器的数据不一致;

    2、LoadingCache 线程池数量配置的不太合理, 导致了任务堆积。

    最终,我们的解决方案是:

    1、异步刷新结合消息机制来更新缓存数据,也就是:当导购服务的配置发生变化时,通知业务网关重新拉取数据,更新缓存。

    2、适当调大 LoadingCache 的线程池参数,并在线程池埋点,监控线程池的使用情况,当线程繁忙时能发出告警,然后动态修改线程池参数。

    4 总结

    Guava Cache 非常强大,但它并没有后台任务线程异步的执行 load 或者 reload 方法,而是通过请求线程来执行相关操作。

    为了提升系统性能,我们可以从如下两个方面来处理 :

    1. 减少过期的频率 ,也就是减少执行 load 方法的频率 ,配置 refresh < expire 。
    2. 采用异步刷新的策略,也就是线程异步加载数据,期间所有请求返回旧的缓存值

    笔者曾经优化过某电商网站的首页接口,使用的方案是: Guava 的异步刷新机制 + 多级缓存 ,取得了非常好得优化效果。

    当然,我们在使用这种方式时,依然需要考虑的数据的一致性问题。


    参考资料:

    https://albenw.github.io/posts/df42dc84/

    如果我的文章对你有所帮助,还请帮忙点赞、在看、转发一下,你的支持会激励我输出更高质量的文章,非常感谢!

  • 相关阅读:
    GIT高级使用技巧
    园区写字楼都在用的物业管理系统
    机器学习中的 K-均值聚类算法及其优缺点。
    API接口签名校验(C#版)
    KT148A语音芯片下载过程的问题集锦 包含下载不了批量生产的说明
    QT添加菜单栏-工具栏-中心区域-状态栏-dock 示范
    嵌入式开发:嵌入式基础——代码和数据空间揭秘
    IDEA调试出现JDWP No transports initialized错误
    项目一共30个模块,你叫我maven版本一个个手动改?
    HttpClient别说话,用心看
  • 原文地址:https://www.cnblogs.com/makemylife/p/17941551