场景
Java核心工具库Guava介绍以及Optional和Preconditions使用进行非空和数据校验:
https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/127683387
在上面引入Guava的基础上。学习其本地缓存Cache的使用。
缓存在很多场景下都是相当有用的。例如,计算或检索一个值的代价很高,
并且对同样的输入需要不止一次获取值的时候,就应当考虑使用缓存。
通常来说,Guava Cache 适用于:
1、你愿意消耗一些内存空间来提升速度。
2、你预料到某些键会被查询一次以上。
3、缓存中存放的数据总量不会超出内存容量。(Guava Cache 是单个应用运行时的本地缓存。它不把数据存放到文件或外部服务器。)
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
关注公众号
霸道的程序猿
获取编程相关电子书、教程推送与免费下载。
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()不会重置缓存项的读取时间。
标签:11,缓存,Java,a2022,示例,Cache,CacheBuilder,build From: https://www.cnblogs.com/badaoliumangqizhi/p/16910712.html