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


    场景

    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

    1.     //模拟从数据库中查询数据
    2.     public User getUserByName(String name){
    3.         return User.builder().name(name).age(20).build();
    4.     }
    5.     @Test
    6.     public void test1(){
    7.         //Guava的缓存创建需要通过CacheBuilder的build()方法构建,它的每个方法都返回CacheBuilder本身,直到build方法被调用才会创建Cache或者LoadingCache
    8.         LoadingCache<String, User> userCache = CacheBuilder.newBuilder()
    9.                 //maximumSize 设置最大存储条数
    10.                 .maximumSize(1000)
    11.                 .build(
    12.                         new CacheLoader<String, User>() {
    13.                             @Override
    14.                             public User load(String name) throws Exception {
    15.                                 //缓存加载逻辑,比如查询数据库等
    16.                                 return getUserByName(name);
    17.                             }
    18.                         }
    19.                 );
    20.         User user = null;
    21.         try {
    22.             //从LoadingCache查询使用get方法。这个方法要么返回已经缓存的值,要么使用CacheLoader向缓存原子地加载新值
    23.             user = userCache.get("霸道的程序猿");
    24.         } catch (ExecutionException e) {
    25.             e.printStackTrace();
    26.         }
    27.         System.out.println(user);//User(name=霸道的程序猿, age=20)
    28.         Cache<String, String> build = CacheBuilder.newBuilder().maximumSize(100).build();
    29.         //放入缓存
    30.         build.put("a","1");
    31.         //获取缓存,如果缓存不存在则返回一个null
    32.         System.out.println(build.getIfPresent("a"));//1
    33.         //所有类型的Guava Cache,不管有没有自动加载功能,都支持get(K,Callable)方法,这个方法返回缓存中的值,如果
    34.         //缓存中没有,则通过Callable进行加载并返回,该操作是原子
    35.         //这个方法简便地实现了模式“如果有缓存则返回;否则运算、缓存、然后返回”
    36.         try {
    37.             String badao = build.get("badao", new Callable<String>() {
    38.                 @Override
    39.                 public String call() throws Exception {
    40.                     return "霸道的程序猿";
    41.                 }
    42.             });
    43.             System.out.println(badao);//霸道的程序猿
    44.         } catch (ExecutionException e) {
    45.             e.printStackTrace();
    46.         }
    47.     }

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

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

    1.     @Test
    2.     public void test2() throws InterruptedException {
    3.         //Guava Cache提供了三种基本的缓存回收方式
    4.         //1、基于容量回收
    5.         //如果要规定缓存项的数目不超过固定值,只需使用CacheBuilder.maximumSize(long)
    6.         //在缓存项的数目达到限定值之前,缓存就可能进行回收操作,通常发生在缓存项的数目逼近限定值时
    7.         //另外不同的缓存项有不同的“权重”(weights)-例如:如果你的缓存值,占据完全不同的内存空间,
    8.         //可以使用CacheBuilder.weigher(Weigher)指定一个权重函数,并且用CacheBuilder.maximumWeight(long)指定最大总重
    9.         Cache<String, User> userCache = CacheBuilder
    10.                 .newBuilder()
    11.                 //设置最大存储容量
    12.                 .maximumWeight(10)
    13.                 .weigher(new Weigher<String, User>() {
    14.                     @Override
    15.                     public int weigh(String s, User user) {
    16.                         //按照年龄大小作为权重计算方式
    17.                         return user.getAge();
    18.                     }
    19.                 }).build();
    20.         int i = 1;
    21.         while (true){
    22.             userCache.put(i+"",User.builder().name(i+"").age(i).build());
    23.             ConcurrentMap<String, User> stringUserConcurrentMap = userCache.asMap();
    24.             System.out.println(stringUserConcurrentMap);
    25.             i++;
    26.             Thread.sleep(1000);
    27.         }
    28.         //运行结果
    29. //       {1=User(name=1, age=1)}
    30. //       {2=User(name=2, age= 2),1=User(name=1, age=1)}
    31. //       {2=User(name=2, age= 2),3=User(name=3, age= 3),1=User(name=1, age=1)}
    32. //       {2=User(name=2, age= 2),3=User(name=3, age= 3),4=User(name=4, age= 4),1=User(name=1, age=1)}
    33. //       {5=User(name=5, age= 5),4=User(name=4, age=4)}
    34. //       {6=User(name=6, age=6)}
    35. //       {7=User(name=7, age=7)}
    36. //       {8=User(name=8, age=8)}
    37. //       {9=User(name=9, age=9)}
    38.     }

    Guava Cache缓存回收-定时回收

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

    1.         Cache<Object, Object> build = CacheBuilder.newBuilder()
    2.                 .maximumSize(100)
    3.                 .expireAfterAccess(5, TimeUnit.SECONDS)
    4.                 .build();
    5.         build.put("a","a");
    6.         while (true){
    7.             System.out.println(build.asMap());
    8.             Thread.sleep(1000);
    9.         }
    10.         //输出结果
    11. //        {a=a}
    12. //        {a=a}
    13. //        {a=a}
    14. //        {a=a}
    15. //        {a=a}
    16. //        {}
    17. //        {}

    Guava Cache缓存回收-手动回收

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

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

    Guava Cache缓存回收-基于引用

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

    1.         Cache<Object, Object> build = CacheBuilder.newBuilder()
    2.                 .weakKeys()
    3.                 .build();
    4.     }

    Guava Cache添加移除监听器

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

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

    Guava Cache刷新

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

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

    Guava Cache 定时刷新

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

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

    1.         LoadingCache<String, String> build = CacheBuilder.newBuilder()
    2.                 .refreshAfterWrite(5, TimeUnit.SECONDS)
    3.                 .build(new CacheLoader<String, String>() {
    4.                     @Override
    5.                     public String load(String s) throws Exception {
    6.                         return s+LocalDateTime.now().toString();
    7.                     }
    8.                 });
    9.         while (true){
    10.             System.out.println(build.get("s"));
    11.             Thread.sleep(1000);
    12.         }
    13.         //输出结果
    14. //        s2022-11-20T13:50:53.369
    15. //        s2022-11-20T13:50:53.369
    16. //        s2022-11-20T13:50:53.369
    17. //        s2022-11-20T13:50:53.369
    18. //        s2022-11-20T13:50:53.369
    19. //        s2022-11-20T13:50:58.429
    20. //        s2022-11-20T13:50:58.429
    21. //        s2022-11-20T13:50:58.429

    注意:

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

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

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

    Guava Cache 异步刷新

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

    1.         ExecutorService executor = Executors.newFixedThreadPool(1);
    2.         LoadingCache<String, String> build = CacheBuilder.newBuilder()
    3.                 .refreshAfterWrite(6, TimeUnit.SECONDS)
    4.                 .build(new CacheLoader<String, String>() {
    5.                     @Override
    6.                     public String load(String s) throws Exception {
    7.                         Thread.sleep(2000);
    8.                         return s+LocalDateTime.now().toString();
    9.                     }
    10.                     @Override
    11.                     public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
    12.                         ListenableFutureTask<String> task = ListenableFutureTask.create(()->load(key));
    13.                         executor.execute(task);
    14.                         return task;
    15.                     }
    16.                 });
    17.         while (true){
    18.             System.out.println(build.get("a"));
    19.             Thread.sleep(1000);
    20.         }
    21.         //模拟客户端一秒调用一次,设置每6秒刷新数据,每次刷新数据需要2秒,在刷新数据期间,获取的仍然是旧值
    22.         //输出结果
    23. //        a2022-11-20T14:46:46.135
    24. //        a2022-11-20T14:46:46.135
    25. //        a2022-11-20T14:46:46.135
    26. //        a2022-11-20T14:46:46.135
    27. //        a2022-11-20T14:46:46.135
    28. //        a2022-11-20T14:46:46.135
    29. //        a2022-11-20T14:46:46.135
    30. //        a2022-11-20T14:46:46.135
    31. //        a2022-11-20T14:46:54.207
    32. //        a2022-11-20T14:46:54.207
    33. //        a2022-11-20T14:46:54.207
    34. //        a2022-11-20T14:46:54.207
    35. //        a2022-11-20T14:46:54.207
    36. //        a2022-11-20T14:46:54.207
    37. //        a2022-11-20T14:46:54.207
    38. //        a2022-11-20T14:46:54.207
    39. //        a2022-11-20T14:46:54.207
    40. //        a2022-11-20T14:47:02.288
    41. //        a2022-11-20T14:47:02.288
    42. //        a2022-11-20T14:47:02.288

    Guava Cache开启统计

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

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

    hitRate():缓存命中率;

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

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

    1.         ExecutorService executor = Executors.newFixedThreadPool(1);
    2.         LoadingCache<String, String> build = CacheBuilder.newBuilder()
    3.                 .refreshAfterWrite(6, TimeUnit.SECONDS)
    4.                 //开启统计
    5.                 .recordStats()
    6.                 .build(new CacheLoader<String, String>() {
    7.                     @Override
    8.                     public String load(String s) throws Exception {
    9.                         Thread.sleep(2000);
    10.                         return s+LocalDateTime.now().toString();
    11.                     }
    12.                     @Override
    13.                     public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
    14.                         ListenableFutureTask<String> task = ListenableFutureTask.create(()->load(key));
    15.                         executor.execute(task);
    16.                         return task;
    17.                     }
    18.                 });
    19.         while (true){
    20.             System.out.println(build.get("a"));
    21.             System.out.println("命中率:"+build.stats().hitCount());
    22.             System.out.println("加载损耗的平均时间:"+build.stats().averageLoadPenalty());
    23.             System.out.println("回收次数:"+build.stats().evictionCount());
    24.             Thread.sleep(1000);
    25.         }
    26.         //输出结果
    27. //        a2022-11-21T10:16:25.759
    28. //        命中率:0
    29. //        加载损耗的平均时间:2.0633746E9
    30. //        回收次数:0
    31. //        a2022-11-21T10:16:25.759
    32. //        命中率:1
    33. //        加载损耗的平均时间:2.0633746E9
    34. //        回收次数:0
    35. //        a2022-11-21T10:16:25.759
    36. //        命中率:2
    37. //        加载损耗的平均时间:2.0633746E9
    38. //        回收次数:0
    39. //        a2022-11-21T10:16:25.759
    40. //        命中率:3
    41. //        加载损耗的平均时间:2.0633746E9
    42. //        回收次数:0
    43. //        a2022-11-21T10:16:25.759
    44. //        命中率:4
    45. //        加载损耗的平均时间:2.0633746E9
    46. //        回收次数: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()不会重置缓存项的读取时间。

  • 相关阅读:
    Vue+elementUI 导出word打印
    C#:in、out、ref关键字
    【Linux】浅谈冯诺依曼体系结构与操作系统
    传奇开服很难吗?教你怎么给Hero传奇引擎添加NPC
    优化导入大批量数据的Excel(上万行的导入)SpringBoot + Apache POI
    SpringBoot系列之搭建WebSocket应用
    D8调试工具——jsvu的使用细则
    Linux1024一篇通俗易懂的liunx命令操作总结(第十课)
    Laravel 的事件监听器与服务提供者和服务容器的二三事
    Java 枚举(enum)学习笔记
  • 原文地址:https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/127960195