Java工具库Guava本地缓存Cache的使用、回收、刷新、统计等示例
admin
2024-02-04 21:33:23

场景

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();}@Testpublic void test1(){//Guava的缓存创建需要通过CacheBuilder的build()方法构建,它的每个方法都返回CacheBuilder本身,直到build方法被调用才会创建Cache或者LoadingCacheLoadingCache userCache = CacheBuilder.newBuilder()//maximumSize 设置最大存储条数.maximumSize(1000).build(new CacheLoader() {@Overridepublic 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 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() {@Overridepublic 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)指定最大总重

    @Testpublic void test2() throws InterruptedException {//Guava Cache提供了三种基本的缓存回收方式//1、基于容量回收//如果要规定缓存项的数目不超过固定值,只需使用CacheBuilder.maximumSize(long)//在缓存项的数目达到限定值之前,缓存就可能进行回收操作,通常发生在缓存项的数目逼近限定值时//另外不同的缓存项有不同的“权重”(weights)-例如:如果你的缓存值,占据完全不同的内存空间,//可以使用CacheBuilder.weigher(Weigher)指定一个权重函数,并且用CacheBuilder.maximumWeight(long)指定最大总重Cache userCache = CacheBuilder.newBuilder()//设置最大存储容量.maximumWeight(10).weigher(new Weigher() {@Overridepublic 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 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 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 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 build = CacheBuilder.newBuilder().weakKeys().build();}

Guava Cache添加移除监听器

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

        Cache build = CacheBuilder.newBuilder().maximumSize(3).removalListener(new RemovalListener() {@Overridepublic void onRemoval(RemovalNotification 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 build = CacheBuilder.newBuilder().refreshAfterWrite(5, TimeUnit.SECONDS).build(new CacheLoader() {@Overridepublic 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 build = CacheBuilder.newBuilder().refreshAfterWrite(6, TimeUnit.SECONDS).build(new CacheLoader() {@Overridepublic String load(String s) throws Exception {Thread.sleep(2000);return s+LocalDateTime.now().toString();}@Overridepublic ListenableFuture reload(String key, String oldValue) throws Exception {ListenableFutureTask 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 build = CacheBuilder.newBuilder().refreshAfterWrite(6, TimeUnit.SECONDS)//开启统计.recordStats().build(new CacheLoader() {@Overridepublic String load(String s) throws Exception {Thread.sleep(2000);return s+LocalDateTime.now().toString();}@Overridepublic ListenableFuture reload(String key, String oldValue) throws Exception {ListenableFutureTask 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()不会重置缓存项的读取时间。

相关内容

热门资讯

真正害死蔡瑁张允的人是谁 具体... 熟读汉末三国史,大家印象中的蔡瑁是一个蛮横专权,一无是处的小人形象。但历史上真正的蔡瑁并非如此,当年...
先胜而后求战:宋庄公为何投资郑... 《孙子兵法》载:“先胜而后求战。”郑庄公当年与宋国开战占尽优势,但其作为一代明主,对郑国所处的地理格...
宋庄公为什么要帮助郑厉公上位?... 上班数年,遇到奇奇葩葩的同事太多,被坑的次数也不少,不过小佛一直秉承做人要厚道的道理。毕竟俺是看了那...
如果不是陆抗 东吴的灭亡时间表... 蜀、魏、吴三国,吴国的情况比较特殊。曹魏挟天子以令诸侯,以禅让的方式,接过汉家权柄。蜀国以汉室正统自...
剃发令虽然成功了,为什么会说差... 顺治元年,即公元1644年,大清王朝在实际带头人多尔衮的领导下,成功入关进驻北京紫禁城。同年,经过王...