1、撚 Google Guava缓存大概有段时间没写文章了,主要是自己掌握的东西没有什么值得讲的,很多的技术都是学着别人的文章学到的,还有一点是自己的技术深度还没有达到要求,有些浮躁,最近代码评审的时候发现自己的代码理解开始回退了,开始写一些逻辑代码了,哈哈哈 不过及时认识到自己的问题
guava 是google 的开源的缓存工具包,基于Java的本地缓存非常的好用,最近也是接触到实际的业务逻辑,看见别人怎么用却总是觉得麻烦难以理解—,— ,(都是这个感觉,不过你真正学会使用之后,就会真香。)
2、♂️快速开始pom文件 -(我使用的是这个版本测试,基本用法应该不会差别太大)
com.google.guava guava21.0
快速测试 - (guava 强大之处在于纯Java代码即可实现使用,可以在任何class中创建出来 (),我们直接创建一个普通的单元测试即可使用
package com.freedom.pangu;
import com.freedom.pangu.dal.dataobject.user.User;
import com.freedom.pangu.dal.repository.test.InfoRepository;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.junit.Test;
import javax.annotation.Resource;
import java.io.File;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
public class baseTest {
private final LoadingCache activityTimingCache = CacheBuilder.newBuilder()
.initialCapacity(150)
.expireAfterWrite(2, TimeUnit.SECONDS)
.refreshAfterWrite(2, TimeUnit.SECONDS)
.concurrencyLevel(5)
.recordStats()
.build(new CacheLoader(){
@Override
public User load(Long id) {
User user = new User();
user.setId(id);
System.out.println("new - "+id);
return user;
}
});
@Test
public void base() throws InterruptedException, ExecutionException {
//第一次获取的时候
System.out.println("get 1-");
activityTimingCache.get(1L);
//一直获取的时候
for (int i=0;i < 7 ;i ++){
//每秒触发一次
Thread.sleep(1000);
System.out.println("get 2-");
activityTimingCache.get(2L);
}
//超时获取的时候 拿到的是新的
System.out.println("get 1-");
activityTimingCache.get(1L);
}
}
然后我们运行程序,打印结果如下,缓存里面没有的,去取的时候会走创建的方法,也就是new -的标识,缓存里面有的时候,则直接取缓存的值。
get 1- new - 1 get 2- new - 2 get 2- get 2- new - 2 get 2- get 2- new - 2 get 2- get 2- new - 2 get 1- new - 13、怜 代码详解
创建的写法基本固定,使用的是build模式,流式处理,也就是可以一直用.来获取到代码提示,直接通过缓存的构建器CacheBuilder来开始构建,
newBuilder实例化一个对象
initialCapacity 初始化缓存容量
expireAfterWrite 过期时间
refreshAfterWrite 在更新之后刷新时间(后面再说)
concurrencyLevel 并发级别
recordStats 统计记录开关
build 构建 需要传入一个CacheLoader的实现类(后面讲)
然后构建完成返回一个缓存对象,推荐使用final 来防止别的程序修改它(不然被改了,你可能突然就用不了了 -。-)
private final LoadingCacheactivityTimingCache = CacheBuilder.newBuilder() .initialCapacity(150) .expireAfterWrite(2, TimeUnit.SECONDS) .refreshAfterWrite(2, TimeUnit.SECONDS) .concurrencyLevel(5) .recordStats() .build(new CacheLoader (){ @Override public User load(Long id) { //...通过key 拿到值 return user; } });
重点就是这几个构建参数,其实看起来很简单,一说也知道怎么用,但是感觉这有啥用啊,不是和我创建一个hashMap一样么?我 put 和get不就好了么?(d _d, 没啥关系,我也是这么想的。)
既然是专门的缓存类,那么他的功能当然比一般的缓存更强大,最强大的一点是拥有过期时间,这个一个缓存器非常具有诱惑的点,大多数情况下,我们并不希望内存被长时间的占用,能够被自动的回收清除
当然构建一个定时删除的容器不是什么难点,但是你自己东拼西凑写一个和功能完善的专门做缓存的缓存类能比较么?(-.-是不是戳自己两下,不知道早点学。)
expireAfterWrite是过期时间,就是一个key的值被缓存这个时间之后就失效了,你再查的时候就又会重新去获取值(比如数据库)
build 里面传递的是CacheLoader的实现类,就是保证缓存没有找到值的时候,应该从哪里去获取最新的值,这样你通过缓存获取的时候永远都有值,而且是相对最新的 (你永远可以相信 *** -。- 网络梗 )
refreshAfterWrite 这个值就比较有意思了,也是个人感觉比redis更好用的一点。
我们平时使用最多是缓存是Redis缓存(比如toke、分布式锁、计数器-次数拦截之类的 权限缓存 菜单缓存 商品缓存 等等),因为Redis是专门做缓存的软件,使用简单,甚至已经出了专门的Redis服务器,但是redis同样也需要成本,需要单独部署,同时Redis放的东西太多了,常常出现Redis挂掉就会影响整个业务的问题(是不是),对于一些简单的、本地缓存更好的,使用guava 缓存是不错的选择
比如开展一个活动的信息存在数据库,我们每次读取都查数据库,那么我们的并发和流量就上不去,但是我们5s 查一次数据库,5s之内从缓存读取,那么在这5s之内,你就是安全的(-!-),5s之后查询的又是数据库信息,你也是新的 (-!-), 完美!
当然5s那一刻我不知道你的生死(-.- ), hhh,也就是常说的缓存击穿,如果访问非常频繁,缓存失效的那一刻打过来的请求因为缓存中查不到值也就都会走数据库查询(默认的获取值的方法,缓存也需要值来更新),那么大量的数据请求就会直接请求数据库,数据库处理不过来,就容易出现宕机重启导致整个系统挂掉的问题
当然也是有办法的,就是在缓存失效之前,我就查好数据,然后在过期之前我把缓存的值更新掉,这样缓存理论上就永不过期了,而且数据也会更新,每次只有一个更新数据的请求,完全无压力,而且guava就提供了解决方案
guava的 refreshAfterWrite 属性就是指定更新值之后刷新值的时间,如果刷新的时间大于有效的时间,那么就是移出key,如果小于有效的时间就更新值。那么就不会出现缓存击穿的问题,我们需要配置的比过期时间小一点,这中间差距的时间就是去更新数据库的数据的时间。
4、 ⬛ 进阶实践redis也可以这样处理,但是一般需要自己去写定时任务更新
expireAfterAccess 访问后过期时间
这个参数和过期时间用法一样,但是表示的含义却是访问之后更新过期时间,如果没有访问则不会更新,可以和过期时间和refreshAfterWrite同时使用
使用我这个版本测试发现:
1、如果访问过期时间比过期时间长,则还是过期时间为准
2、如果访问过期时间比过期时间短,则会以访问过期时间为准
3、简单总结一下就是那个时间短就会优先过期,(可以自己试试)
removalListener过期监听器,这个用法看下运行结果就知道了,代码如下
private final LoadingCacheactivityTimingCache = CacheBuilder.newBuilder() .initialCapacity(2) .expireAfterWrite(15, TimeUnit.SECONDS) .expireAfterAccess(2,TimeUnit.SECONDS) .refreshAfterWrite(3, TimeUnit.SECONDS) .removalListener(notification -> { System.out.println("get remove"+notification); }) .concurrencyLevel(5) .recordStats() .build(new CacheLoader (){ @Override public User load(Long id) { User user = new User(); user.setId(id); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("new - "+id); return user; } }); @Test public void base() throws InterruptedException, ExecutionException { //第一次获取的时候 System.out.println("get 1-"); activityTimingCache.get(1L); activityTimingCache.get(1L); //一直获取的时候 Thread.sleep(9000); //超时获取的时候 拿到的是新的 System.out.println("get 1-"); activityTimingCache.get(1L); }
结果却很有意思,注意这个移出的监听器触发的时间是9s之后访问的时候触发的,就是说guava 是在访问的时候才会去移除对应的key,而且监听器拿到的是value而不是key.
get 1- new - 1 get 1- get remove1=User(super=baseDO(id=1, deleted=null, version=null, addTime=null, updateTime=null, createdBy=null, lastModifiedBy=null), name=null, pwd=null, role=null, status=0, lastLoginTime=null, lastLoginIp=null, token=null) new - 15、 设计分析
整个缓存类使用Java编写,不需要额外的添加三方软件和程序,使用当前主机的内存,适用性非常强
使用了泛型类,支持各种的key value,基本可以缓存所有的对象,而redis基本都是保存为字符串序列化转化为对象,当然看起来结果是一样。
使用代码非常少,基本上核心参数都可以直接创建的时候构造,无需写配置类等,拔插性非常高,可以多次构建,灵活使用。
开源,大公司出品的开源程序,真的都是自己在用的程序。
好用,解决大部分问题,甚至考虑到缓存失效。



