Java工具库Guava本地缓存Cache的使用、回收、刷新、统计等示例

生活中,最使人疲惫的往往不是道路的遥远,而是心中的郁闷;最使人痛苦的往往不是生活的不幸,而是希望的破灭;最使人颓废的往往不是前途的坎坷,而是自信的丧失;最使人绝望的往往不是挫折的打击,而是心灵的死亡。所以我们要有自己的梦想,让梦想的星光指引着我们走出落漠,走出惆怅,带着我们走进自己的理想。

导读:本篇文章讲解 Java工具库Guava本地缓存Cache的使用、回收、刷新、统计等示例,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

场景

Java核心工具库Guava介绍以及Optional和Preconditions使用进行非空和数据校验:

Java核心工具库Guava介绍以及Optional和Preconditions使用进行非空和数据校验_霸道流氓气质的博客-CSDN博客

在上面引入Guava的基础上。学习其本地缓存Cache的使用。

缓存在很多场景下都是相当有用的。例如,计算或检索一个值的代价很高,

并且对同样的输入需要不止一次获取值的时候,就应当考虑使用缓存。

通常来说,Guava Cache 适用于:

1、你愿意消耗一些内存空间来提升速度。

2、你预料到某些键会被查询一次以上。

3、缓存中存放的数据总量不会超出内存容量。(Guava Cache 是单个应用运行时的本地缓存。它不把数据存放到文件或外部服务器。)

注:

博客:
霸道流氓气质的博客_CSDN博客-C#,架构之路,SpringBoot领域博主
关注公众号
霸道的程序猿
获取编程相关电子书、教程推送与免费下载。

Guava的缓存Cache创建和存取

Guava的缓存创建需要通过CacheBuilder的build()方法构建,它的每个方法都返回CacheBuilder本身,

直到build方法被调用才会创建Cache或者LoadingCache

    //模拟从数据库中查询数据
    public User getUserByName(String name){
        return User.builder().name(name).age(20).build();
    }
    @Test
    public void test1(){

        //Guava的缓存创建需要通过CacheBuilder的build()方法构建,它的每个方法都返回CacheBuilder本身,直到build方法被调用才会创建Cache或者LoadingCache
        LoadingCache<String, User> userCache = CacheBuilder.newBuilder()
                //maximumSize 设置最大存储条数
                .maximumSize(1000)
                .build(
                        new CacheLoader<String, User>() {
                            @Override
                            public User load(String name) throws Exception {
                                //缓存加载逻辑,比如查询数据库等
                                return getUserByName(name);
                            }
                        }
                );

        User user = null;
        try {
            //从LoadingCache查询使用get方法。这个方法要么返回已经缓存的值,要么使用CacheLoader向缓存原子地加载新值
            user = userCache.get("霸道的程序猿");
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println(user);//User(name=霸道的程序猿, age=20)

        Cache<String, String> build = CacheBuilder.newBuilder().maximumSize(100).build();
        //放入缓存
        build.put("a","1");
        //获取缓存,如果缓存不存在则返回一个null值
        System.out.println(build.getIfPresent("a"));//1

        //所有类型的Guava Cache,不管有没有自动加载功能,都支持get(K,Callable)方法,这个方法返回缓存中的值,如果
        //缓存中没有,则通过Callable进行加载并返回,该操作是原子
        //这个方法简便地实现了模式“如果有缓存则返回;否则运算、缓存、然后返回”
        try {
            String badao = build.get("badao", new Callable<String>() {
                @Override
                public String call() throws Exception {
                    return "霸道的程序猿";
                }
            });
            System.out.println(badao);//霸道的程序猿
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

Guava Cache缓存回收-基于容量回收

基于容量回收
如果要规定缓存项的数目不超过固定值,只需使用CacheBuilder.maximumSize(long)
在缓存项的数目达到限定值之前,缓存就可能进行回收操作,通常发生在缓存项的数目逼近限定值时
另外不同的缓存项有不同的“权重”(weights)-例如:如果你的缓存值,占据完全不同的内存空间,
可以使用CacheBuilder.weigher(Weigher)指定一个权重函数,并且用CacheBuilder.maximumWeight(long)指定最大总重

    @Test
    public void test2() throws InterruptedException {

        //Guava Cache提供了三种基本的缓存回收方式
        //1、基于容量回收
        //如果要规定缓存项的数目不超过固定值,只需使用CacheBuilder.maximumSize(long)
        //在缓存项的数目达到限定值之前,缓存就可能进行回收操作,通常发生在缓存项的数目逼近限定值时
        //另外不同的缓存项有不同的“权重”(weights)-例如:如果你的缓存值,占据完全不同的内存空间,
        //可以使用CacheBuilder.weigher(Weigher)指定一个权重函数,并且用CacheBuilder.maximumWeight(long)指定最大总重
        Cache<String, User> userCache = CacheBuilder
                .newBuilder()
                //设置最大存储容量
                .maximumWeight(10)
                .weigher(new Weigher<String, User>() {
                    @Override
                    public int weigh(String s, User user) {
                        //按照年龄大小作为权重计算方式
                        return user.getAge();
                    }
                }).build();

        int i = 1;

        while (true){
            userCache.put(i+"",User.builder().name(i+"").age(i).build());
            ConcurrentMap<String, User> stringUserConcurrentMap = userCache.asMap();
            System.out.println(stringUserConcurrentMap);
            i++;
            Thread.sleep(1000);
        }

        //运行结果
//       {1=User(name=1, age=1)}
//       {2=User(name=2, age= 2),1=User(name=1, age=1)}
//       {2=User(name=2, age= 2),3=User(name=3, age= 3),1=User(name=1, age=1)}
//       {2=User(name=2, age= 2),3=User(name=3, age= 3),4=User(name=4, age= 4),1=User(name=1, age=1)}
//       {5=User(name=5, age= 5),4=User(name=4, age=4)}
//       {6=User(name=6, age=6)}
//       {7=User(name=7, age=7)}
//       {8=User(name=8, age=8)}
//       {9=User(name=9, age=9)}
    }

Guava Cache缓存回收-定时回收

CacheBuilder提供两种定时回收的方法:
expireAfterAccess(long,TimeUnit):缓存项在给定时间内没有被读/写访问,则回收
expireAfterWriter(long,TimeUnit):缓存项在给定时间内没有被写访问(创建或覆盖)

        Cache<Object, Object> build = CacheBuilder.newBuilder()
                .maximumSize(100)
                .expireAfterAccess(5, TimeUnit.SECONDS)
                .build();

        build.put("a","a");
        while (true){
            System.out.println(build.asMap());
            Thread.sleep(1000);
        }

        //输出结果
//        {a=a}
//        {a=a}
//        {a=a}
//        {a=a}
//        {a=a}
//        {}
//        {}

Guava Cache缓存回收-手动回收

invalidate(key)回收个别指定
invalidataeAll(keys)批量回收
invalidataeAll()回收所有

        Cache<Object, Object> build = CacheBuilder.newBuilder()
                .build();
        for (int i = 0; i < 10 ; i++) {
            build.put(i,i);
            if(i % 2 == 0){
                build.invalidate(i);
            }
        }
        System.out.println(build.asMap());//{5=5, 7=7, 1=1, 9=9, 3=3}
        build.invalidateAll();
        System.out.println(build.asMap());//{}

    }

Guava Cache缓存回收-基于引用

//        4、通过weakKeys和weakValues方法指定Cache只保存对缓存记录key和value的弱引用。
//        这样当没有其他强引用指向key和value时,key和value对象就会被垃圾回收器回收。
//
//        CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。
//       因为垃圾回收仅依赖恒等式(==),使用弱引用键的缓存用==而不是equals比较键。
//        CacheBuilder.weakValues():使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收。
//       因为垃圾回收仅依赖恒等式(==),使用弱引用值的缓存用==而不是equals比较值。
//        CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。
//        考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定(见上文,基于容量回收)。
//        使用软引用值的缓存同样用==而不是equals比较值。

        Cache<Object, Object> build = CacheBuilder.newBuilder()
                .weakKeys()
                .build();
    }

Guava Cache添加移除监听器

可以为Cache对象添加一个移除监听器,以便缓存被移除时做一些额外操作。
缓存项被移除时,RemovalListener会获取通知RemovalNotification,其中包含移除原因RemovalCause、键和值

        Cache<String, String> build = CacheBuilder.newBuilder()
                .maximumSize(3)
                .removalListener(new RemovalListener<String, String>() {
                    @Override
                    public void onRemoval(RemovalNotification<String, String> notification) {
                        //输出结果:cause:EXPLICIT key:a value:a被移除
                        System.out.println("cause:"+notification.getCause()+" key:"+notification.getKey()+" value:"+notification.getValue()+"被移除");
                    }
                })
                .build();
        build.put("a","a");
        build.invalidate("a");

Guava Cache刷新

刷新和回收不太一样。正如LoadingCache.refresh(K)所声明,刷新表示为键加载新值,这个过程可以是异步的。

在刷新操作进行时,缓存仍然可以向其他线程返回旧值,而不像回收操作,读缓存的线程必须等待新值加载完成。

Guava Cache 定时刷新

在进行缓存定时刷新时,需要指定缓存的刷新间隔,和一个用来加载缓存的CacheLoader,当达到刷新时间间隔后,

下一次获取缓存时,会调用CacheLoader的load方法刷新缓存

        LoadingCache<String, String> build = CacheBuilder.newBuilder()
                .refreshAfterWrite(5, TimeUnit.SECONDS)
                .build(new CacheLoader<String, String>() {
                    @Override
                    public String load(String s) throws Exception {
                        return s+LocalDateTime.now().toString();
                    }
                });

        while (true){
            System.out.println(build.get("s"));
            Thread.sleep(1000);
        }
        //输出结果
//        s2022-11-20T13:50:53.369
//        s2022-11-20T13:50:53.369
//        s2022-11-20T13:50:53.369
//        s2022-11-20T13:50:53.369
//        s2022-11-20T13:50:53.369
//        s2022-11-20T13:50:58.429
//        s2022-11-20T13:50:58.429
//        s2022-11-20T13:50:58.429

注意:

缓存项只有在被检索时才会真正刷新(如果CacheLoader.refresh实现为异步,那么检索不会被刷新拖慢)

因此,如果你在缓存上同时声明expireAfterWrite和refreshAfterWrite,缓存并不会因为刷新盲目地定时重置

如果缓存项没有被检索,那刷新就不会真的发生,缓存项在过期时间后也会变得可以回收

Guava Cache 异步刷新

在刷新操作进行时,缓存仍然可以向其他线程返回旧值,而不像回收操作,读缓存的操作必须等待新值加载完成。

        ExecutorService executor = Executors.newFixedThreadPool(1);
        LoadingCache<String, String> build = CacheBuilder.newBuilder()
                .refreshAfterWrite(6, TimeUnit.SECONDS)
                .build(new CacheLoader<String, String>() {
                    @Override
                    public String load(String s) throws Exception {
                        Thread.sleep(2000);
                        return s+LocalDateTime.now().toString();
                    }

                    @Override
                    public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
                        ListenableFutureTask<String> task = ListenableFutureTask.create(()->load(key));
                        executor.execute(task);
                        return task;
                    }
                });

        while (true){
            System.out.println(build.get("a"));
            Thread.sleep(1000);
        }

        //模拟客户端一秒调用一次,设置每6秒刷新数据,每次刷新数据需要2秒,在刷新数据期间,获取的仍然是旧值
        //输出结果
//        a2022-11-20T14:46:46.135
//        a2022-11-20T14:46:46.135
//        a2022-11-20T14:46:46.135
//        a2022-11-20T14:46:46.135
//        a2022-11-20T14:46:46.135
//        a2022-11-20T14:46:46.135
//        a2022-11-20T14:46:46.135
//        a2022-11-20T14:46:46.135
//        a2022-11-20T14:46:54.207
//        a2022-11-20T14:46:54.207
//        a2022-11-20T14:46:54.207
//        a2022-11-20T14:46:54.207
//        a2022-11-20T14:46:54.207
//        a2022-11-20T14:46:54.207
//        a2022-11-20T14:46:54.207
//        a2022-11-20T14:46:54.207
//        a2022-11-20T14:46:54.207
//        a2022-11-20T14:47:02.288
//        a2022-11-20T14:47:02.288
//        a2022-11-20T14:47:02.288

Guava Cache开启统计

CacheBuilder.recordStats()用来开启 Guava Cache 的统计功能。

统计打开后,Cache.stats()方法会返回CacheStats对象以提供如下统计信息:

hitRate():缓存命中率;

averageLoadPenalty():加载新值的平均时间,单位为纳秒;

evictionCount():缓存项被回收的总数,不包括显式清除。

        ExecutorService executor = Executors.newFixedThreadPool(1);
        LoadingCache<String, String> build = CacheBuilder.newBuilder()
                .refreshAfterWrite(6, TimeUnit.SECONDS)
                //开启统计
                .recordStats()
                .build(new CacheLoader<String, String>() {
                    @Override
                    public String load(String s) throws Exception {
                        Thread.sleep(2000);
                        return s+LocalDateTime.now().toString();
                    }

                    @Override
                    public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
                        ListenableFutureTask<String> task = ListenableFutureTask.create(()->load(key));
                        executor.execute(task);
                        return task;
                    }
                });

        while (true){
            System.out.println(build.get("a"));
            System.out.println("命中率:"+build.stats().hitCount());
            System.out.println("加载损耗的平均时间:"+build.stats().averageLoadPenalty());
            System.out.println("回收次数:"+build.stats().evictionCount());
            Thread.sleep(1000);
        }

        //输出结果
//        a2022-11-21T10:16:25.759
//        命中率:0
//        加载损耗的平均时间:2.0633746E9
//        回收次数:0
//        a2022-11-21T10:16:25.759
//        命中率:1
//        加载损耗的平均时间:2.0633746E9
//        回收次数:0
//        a2022-11-21T10:16:25.759
//        命中率:2
//        加载损耗的平均时间:2.0633746E9
//        回收次数:0
//        a2022-11-21T10:16:25.759
//        命中率:3
//        加载损耗的平均时间:2.0633746E9
//        回收次数:0
//        a2022-11-21T10:16:25.759
//        命中率:4
//        加载损耗的平均时间:2.0633746E9
//        回收次数:0

Guava Cache asMap视图

asMap 视图提供了缓存的 ConcurrentMap 形式,但 asMap 视图与缓存的交互需要注意:

1、cache.asMap()包含当前所有加载到缓存的项。因此相应地,cache.asMap().keySet()包含当前所有已加载键;

2、asMap().get(key)实质上等同于 cache.getIfPresent(key),而且不会引起缓存项的加载。这和 Map 的语义约定一致。

3、所有读写操作都会重置相关缓存项的访问时间,包括 Cache.asMap().get(Object)方法和 Cache.asMap().put(K, V)方法,

但不包括 Cache.asMap().containsKey(Object)方法,也不包括在 Cache.asMap()的集合视图上的操作。

比如,遍历 Cache.asMap().entrySet()不会重置缓存项的读取时间。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/135862.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!