Guava:Cache强大的本地缓存框架

这篇具有很好参考价值的文章主要介绍了Guava:Cache强大的本地缓存框架。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

Guava Cache是一款非常优秀的本地缓存框架。

一、 经典配置

Guava Cache 的数据结构跟 JDK1.7 的 ConcurrentHashMap 类似,提供了基于时间、容量、引用三种回收策略,以及自动加载、访问统计等功能。

Guava:Cache强大的本地缓存框架,Guava,常用,学习日记,guava,缓存,java

基本的配置

    @Test
    public void testLoadingCache() throws ExecutionException {
        CacheLoader<String, String> cacheLoader = new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                System.out.println("加载 key:" + key);
                return "value";
            }
        };

        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                //最大容量为100(基于容量进行回收)
                .maximumSize(100)
                //配置写入后多久使缓存过期
                .expireAfterWrite(10, TimeUnit.SECONDS)
                //配置写入后多久刷新缓存
                .refreshAfterWrite(1, TimeUnit.SECONDS)
                .build(cacheLoader);

        cache.put("Lasse", "穗爷");
        System.out.println(cache.size());
        System.out.println(cache.get("Lasse"));
        System.out.println(cache.getUnchecked("hello"));
        System.out.println(cache.size());

    }

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

1、失效策略

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

2、刷新策略

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

这个例子里,有的同学可能会有疑问:为什么需要配置刷新策略,只配置失效策略不就可以吗

当然是可以的,但在高并发场景下,配置刷新策略会有奇效,接下来,我们会写一个测试用例,方便大家理解 Gauva Cache 的线程模型。

二、理解线程模型

我们模拟在多线程场景下,「缓存过期执行 load 方法」和「刷新执行 reload 方法」两者的运行情况。

@Test
    public void testLoadingCache2() throws InterruptedException, ExecutionException {
        CacheLoader<String, String> cacheLoader = new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                System.out.println(Thread.currentThread().getName() + "加载 key" + key);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                return "value_" + key.toLowerCase();
            }

            @Override
            public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
                System.out.println(Thread.currentThread().getName() + "加载 key" + key);
                Thread.sleep(500);
                return super.reload(key, oldValue);
            }
        };
        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                //最大容量为20(基于容量进行回收)
                .maximumSize(20)
                //配置写入后多久使缓存过期
                .expireAfterWrite(10, TimeUnit.SECONDS)
                //配置写入后多久刷新缓存
                .refreshAfterWrite(1, TimeUnit.SECONDS)
                .build(cacheLoader);

        System.out.println("测试过期加载 load------------------");

        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        long start = System.currentTimeMillis();
                        System.out.println(Thread.currentThread().getName() + "开始查询");
                        String hello = cache.get("hello");
                        long end = System.currentTimeMillis() - start;
                        System.out.println(Thread.currentThread().getName() + "结束查询 耗时" + end);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            });
        }

        cache.put("hello2", "旧值");
        Thread.sleep(2000);
        System.out.println("测试重新加载 reload");
        //等待刷新,开始重新加载
        Thread.sleep(1500);
        ExecutorService executorService2 = Executors.newFixedThreadPool(5);
//        CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
        for (int i = 0; i < 5; i++) {
            executorService2.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        long start = System.currentTimeMillis();
                        System.out.println(Thread.currentThread().getName() + "开始查询");
                        //cyclicBarrier.await();
                        String hello = cache.get("hello2");
                        System.out.println(Thread.currentThread().getName() + ":" + hello);
                        long end = System.currentTimeMillis() - start;
                        System.out.println(Thread.currentThread().getName() + "结束查询 耗时" + end);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            });
        }
        Thread.sleep(9000);
    }

 执行结果见下图

Guava:Cache强大的本地缓存框架,Guava,常用,学习日记,guava,缓存,java

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

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

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

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

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

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

  1. 配置  refresh < expire ,减少大量线程阻塞的概率;

  2. 采用异步刷新的策略,也就是线程异步加载数据,期间所有请求返回旧的缓存值,防止缓存雪崩。

下图展示优化方案的时间轴 :

Guava:Cache强大的本地缓存框架,Guava,常用,学习日记,guava,缓存,java

三、 两种方式实现异步刷新

3.1 重写 reload 方法

ExecutorService executorService = Executors.newFixedThreadPool(5);
        CacheLoader<String, String> cacheLoader = new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                System.out.println(Thread.currentThread().getName() + "加载 key" + key);
                //从数据库加载
                return "value_" + key.toLowerCase();
            }

            @Override
            public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
                ListenableFutureTask<String> futureTask = ListenableFutureTask.create(() -> {
                    System.out.println(Thread.currentThread().getName() + "异步加载 key" + key);
                    return load(key);
                });
                executorService.submit(futureTask);
                return futureTask;
            }
        };
        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                //最大容量为20(基于容量进行回收)
                .maximumSize(20)
                //配置写入后多久使缓存过期
                .expireAfterWrite(10, TimeUnit.SECONDS)
                //配置写入后多久刷新缓存
                .refreshAfterWrite(1, TimeUnit.SECONDS)
                .build(cacheLoader);

3.2 实现 asyncReloading 方法

ExecutorService executorService = Executors.newFixedThreadPool(5);

        CacheLoader.asyncReloading(
                new CacheLoader<String, String>() {
                    @Override
                    public String load(String key) throws Exception {
                        System.out.println(Thread.currentThread().getName() + "加载 key" + key);
                        //从数据库加载
                        return "value_" + key.toLowerCase();
                    }
                }
                , executorService);

四、异步刷新 + 多级缓存

场景

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

整体架构如下图所示:

Guava:Cache强大的本地缓存框架,Guava,常用,学习日记,guava,缓存,java

缓存读取流程如下

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 的线程池参数,并在线程池埋点,监控线程池的使用情况,当线程繁忙时能发出告警,然后动态修改线程池参数。

五、总结

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

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

  1. 配置 refresh < expire,减少大量线程阻塞的概率。

  2. 采用异步刷新的策略,也就是线程异步加载数据,期间所有请求返回旧的缓存值

尽管如此,我们在使用这种方式时,依然需要考虑的缓存和数据库一致性问题。 文章来源地址https://www.toymoban.com/news/detail-781982.html

到了这里,关于Guava:Cache强大的本地缓存框架的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Spring | Spring Cache 缓存框架

    Spring Cache 是 Spring 的一个框架, 实现了基于注解的缓存功能 。只需简单加一个 注解 ,就能实现 缓存功能 。 Spring Cache提供了一层抽象 , 底层可以切换不同的缓存实现 。比较 常见 的(底层) 缓存实现 有: Redis、EHCache、Caffeine 。可自定义地修改 Spring Cache 底层的 缓存实现 。

    2024年02月08日
    浏览(46)
  • rocketmq客户端本地日志文件过大调整配置(导致pod缓存cache过高)

            在使用rocketmq时,发现本地项目中文件越来越大,查找发现在/home/root/logs/rocketmqlog目录下存在大量rocketmq_client.log日志文件。 开启slf4j日志模式,在项目启动项中增加-Drocketmq.client.logUseSlf4j=true 因为配置使用的是System.getProperty获取,所以只能使用系统环境配置。 调整日

    2024年02月15日
    浏览(41)
  • Guava Cache 介绍

    Guava 是 Google 提供的一套 Java 工具包,而 Guava Cache 是该工具包中提供的一套完善的 JVM 级别高并发缓存框架;本文主要介绍它的相关功能及基本使用,文中所使用到的软件版本:Java 1.8.0_341、Guava 32.1.3-jre。 缓存在很多情况下非常有用。例如,当某个值的计算或检索代价很高,

    2024年02月05日
    浏览(46)
  • Guava Cache介绍-面试用

    Guava Cache是本地缓存,数据读写都在一个进程内,相对于分布式缓存redis,不需要网络传输的过程,访问速度很快,同时也受到 JVM 内存的制约,无法在数据量较多的场景下使用。 基于以上特点,本地缓存的主要应用场景为以下几种: 对于访问速度有较大要求 存储的数据不经

    2024年02月07日
    浏览(40)
  • cache2k:Guava Cache及Caffeine之外的新选择

    本文主要研究一下cache2k这款新型缓存 JCache规范不支持null,所以cache2k默认也不支持,不过可以通过permitNullValues(true)来开启,这样子缓存就可以存储null值 又称作cache miss storm,指的是高并发场景缓存同时失效导致大面积回源,cache2k采用的是block的请求方式,避免对同一个key并

    2024年02月03日
    浏览(41)
  • flink1.15 维表join guava cache和mysql方面优化

    优化前  mysql响应慢,导致算子中数据输出追不上输入,导致显示cpu busy:100% 优化后效果两个图对应两个时刻: - - 图中guava cache命中率是通过guava自带统计,打印出来的. 1 guava缓存数据量上限 = 类中配置的guava缓存数据上线 * task个数(即flink并行度) 缓存越久 命中率越高 数据越陈旧

    2024年01月17日
    浏览(38)
  • 【论文解读|GL-Cache 】基于组级学习的缓存替换算法

    论文原文: GL-Cache: Group-level learning for efficient and high-performance caching | FAST \\\'23 源码   地址: GitHub - Thesys-lab/fast23-GLCache: Repository for FAST\\\'23 paper GL-Cache: Group-level Learning for Efficient and High-Performance Caching 论文贡献: 提出 Group-level Learning ,利用多对象组的特征来适应工作负荷和缓存

    2024年02月07日
    浏览(48)
  • 【go语言开发】本地缓存的使用,从简单到复杂写一个本地缓存,并对比常用的开源库

    本文主要介绍go语言中本地缓存的使用,首先由简单到复杂手写3个本地缓存示例,使用内置的sync,map等数据结构封装cache,然后介绍常见的一些开源库,以及对比常用的开源库 本地缓存 是指将一部分数据存储在应用程序本地内存中,以提高数据访问速度和应用程序性能的技

    2024年02月04日
    浏览(36)
  • gem5学习(11):将缓存添加到配置脚本中——Adding cache to the configuration script

    目录 一、Creating cache objects 1、Classic caches and Ruby 二、Cache 1、导入SimObject(s) 2、创建L1Cache 3、创建L1Cache子类 4、创建L2Cache 5、L1Cache添加连接函数 6、为L1ICache和L1DCache添加连接函数 7、为L2Cache添加内存侧和CPU侧的连接函数 完整代码 三、Adding caches to the simple config file 1、导入cac

    2024年01月25日
    浏览(44)
  • 【java缓存、redis缓存、guava缓存】java中实现缓存的几种方式

    这种方式可以简单实现本地缓存,但是实际开发中不推荐使用,下面我们来实现一下这种方式。 首先创建一个管理缓存的类 这个类中有一个静态代码块,静态代码块会在类加载时就执行,我们可以在这里完成对缓存的初始化,决定缓存内一开始就有哪些数据 另外我们还可以

    2024年02月16日
    浏览(35)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包