Java核心工具库Guava介绍以及Optional和Preconditions使用进行非空和数据校验:
Java核心工具库Guava介绍以及Optional和Preconditions使用进行非空和数据校验_霸道流氓气质的博客-CSDN博客
在上面引入Guava的基础上。学习其本地缓存Cache的使用。
缓存在很多场景下都是相当有用的。例如,计算或检索一个值的代价很高,
并且对同样的输入需要不止一次获取值的时候,就应当考虑使用缓存。
通常来说,Guava Cache 适用于:
1、你愿意消耗一些内存空间来提升速度。
2、你预料到某些键会被查询一次以上。
3、缓存中存放的数据总量不会超出内存容量。(Guava Cache 是单个应用运行时的本地缓存。它不把数据存放到文件或外部服务器。)
注:
博客:
霸道流氓气质的博客_CSDN博客-C#,架构之路,SpringBoot领域博主
关注公众号
霸道的程序猿
获取编程相关电子书、教程推送与免费下载。
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();
- }
- }
基于容量回收
如果要规定缓存项的数目不超过固定值,只需使用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)}
- }
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}
- // {}
- // {}
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());//{}
-
- }
// 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();
- }
可以为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");
刷新和回收不太一样。正如LoadingCache.refresh(K)所声明,刷新表示为键加载新值,这个过程可以是异步的。
在刷新操作进行时,缓存仍然可以向其他线程返回旧值,而不像回收操作,读缓存的线程必须等待新值加载完成。
在进行缓存定时刷新时,需要指定缓存的刷新间隔,和一个用来加载缓存的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,缓存并不会因为刷新盲目地定时重置
如果缓存项没有被检索,那刷新就不会真的发生,缓存项在过期时间后也会变得可以回收
在刷新操作进行时,缓存仍然可以向其他线程返回旧值,而不像回收操作,读缓存的操作必须等待新值加载完成。
- 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
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
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()不会重置缓存项的读取时间。