Guava 本地缓存介绍, 以及缓存主动刷新缓存功能
今天来搞一搞guava cache中的手动刷新刷新功能
Guava cache 没了解的小伙伴可以先自行了解下, 今天这里主要来聊一聊缓存的主动刷新
我们先来看一看一个简单缓存的示例
/**
* @author : zmm
* @description :
* @date : 2023-10-30 14:05
**/
@Slf4j
@Component
public class TestCache implements InitializingBean, DisposableBean {
/**
* 缓存
*/
private static LoadingCache<String, String> cache = null;
/**
* 获取缓存
*
* @param key
* @return
*/
public static String getTestCache(String key) {
try {
String value = cache.get(key);
return value;
} catch (ExecutionException e) {
return null;
}
}
@Override
public void destroy() throws Exception {
Optional.ofNullable(cache).ifPresent(Cache::cleanUp);
}
/**
* 重写 InitializingBean 的方法 加载配置文件后调用此方方法
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
init();
}
/**
* 初始化缓存
*/
private void init() {
cache = CacheBuilder.newBuilder()
//缓存池大小,在缓存项接近该大小时, Guava开始回收旧的缓存项
.maximumSize(1000)
//设置时间对象没有被读/写访问则对象从内存中删除
.expireAfterAccess(1, TimeUnit.HOURS)
//移除监听器,缓存项被移除时会触发
.removalListener(new TestCacheRemoveListener())
//.recordStats()//开启Guava Cache的统计功能
.build(new TestCacheLoader());
}
/**
* 缓存加载类
*/
private class TestCacheLoader extends CacheLoader<String, String> {
@Override
public String load(String key) throws Exception {
//这里从数据库查询放入返回,会放进缓存
//String data = service.getbyId();
return "data";
}
}
/**
* 移除监听器,当缓存被移除时执行的动作
*/
private class TestCacheRemoveListener implements RemovalListener<String, String> {
@Override
public void onRemoval(RemovalNotification<String, String> removalNotification) {
log.info("key:{},value:{} removed", removalNotification.getKey(), removalNotification.getValue());
}
}
}
我们在启动项目的时候初始化了缓存 初始容量是1000 然后缓存的时间是1小时;
然后在项目里可以调用静态方法 Test.getTestCache("test"); 来获取缓存;
缓存(cache)里没值的话会自动调用load方法查询业务数据放进缓存cache
上面描述的是最基本的一个缓存使用
还有一些场景:
假设我有一个配置表使用了guavacache做了缓存, 当我新增了一条配置信息时是没问题的,因为当cache- 新增的key不存在时会调用 load 方法去业务库查询缓存,
但是当我对配置进行修改的时候 比如我把某一条配置的 属性 从1修改为2,在缓存没有刷新之前程序一直拿到的值还会是1 这样就会对我们业务有一定的影响 ,数据不实时
很显然 guava没有提供刷新缓存的功能
那我们自己思考如何实现呢?
思路1:
重新初始化缓存,直接调用init方法重置缓存 (此方法过于暴力,不推荐), 这种方式不能使用静态方法,需要先注入TestCache
private TestCache testCache;
@Slf4j
@Component
public class TestCache implements InitializingBean, DisposableBean {
/**
* 缓存
*/
private static LoadingCache<String, String> cache = null;
/**
* 强制刷新缓存-不推荐
*/
public void refreshCache() {
this.init();
}
.......
思路2:
既然缓存的逻辑是: 先取map中的值, map中没有的就回去数据库查询 那么我们可以想办法把想刷新的map中的key remove掉
@Slf4j
@Component
public class TestCache implements InitializingBean, DisposableBean {
/**
* 缓存
*/
private static LoadingCache<String, String> cache = null;
/**
* 删除缓存-刷新
*/
public static void refreshCache2() {
long start = System.currentTimeMillis();
ConcurrentMap<String, String> map = cache.asMap();
map.keySet().forEach(item -> map.remove(item));
log.info("刷新缓存耗时:{}", System.currentTimeMillis() - start);
}
但是这里有一个问题: 就是如果我在删除的过程中如果程序一直在使用缓存就会有问题,可能会边删除边插入, 有问题肯定有解决方法,那么我们只需要把这个方法进行一点优化
简单思路: 使用AtomicBoolean 对刷新缓存的过程进行加锁, 在刷新完毕后再解锁, 此过程中获取缓存会等待,这个速度很快随意可以忽略对缓存的获取的影响
AtomicBoolean 是一个原子操作的boolean类型 感兴趣的可以了解下,简单说就是线程安全的 boolean 具有原子操作
@Slf4j
@Component
public class TestCache implements InitializingBean, DisposableBean {
/**
* 缓存
*/
private static LoadingCache<String, String> cache = null;
/**
* 刷新缓存锁
*/
private static AtomicBoolean REFRESH_CACHING = new AtomicBoolean(false);
/**
* 删除缓存-刷新
*/
public static void refreshCache2() {
long start = System.currentTimeMillis();
try {
ConcurrentMap<String, String> map = cache.asMap();
//新增一个刷新缓存锁,这个boolean具有原子操作 线程安全,开始刷新缓存时,我们需要把锁开启,结束后关闭锁,期间不允许调用缓存即可,这个过程很快所以无需担心获取缓存慢
boolean lock = REFRESH_CACHING.compareAndSet(false, true);
if (lock) {
map.keySet().forEach(item -> map.remove(item));
REFRESH_CACHING.set(false);
} else {
log.error("正在刷新,请等待");
}
} catch (Exception e) {
log.error("刷新缓存异常", e);
//这里刷新缓存失败,保证锁释放不影响缓存调用
REFRESH_CACHING.set(false);
}
log.info("刷新缓存耗时:{}", System.currentTimeMillis() - start);
}
/**
* 获取缓存-新增自旋锁 缓存刷新期间等待
*
* @param key
* @return
*/
public static String getTestCache(String key)throws Exception {
while (true){
if (!REFRESH_CACHING.get()){
return cache.get(key);
}
log.info("缓存刷新中,自旋等待");
Thread.sleep(200);
}
}
......
获取缓存的时候先判断锁的状态,当前是否正在刷新缓存,如果是 那么将自旋等待
当只需要刷新某个缓存时:
/**
* 删除缓存-刷新,刷新指定的key
*/
public static void refreshCache2(String key) {
long start = System.currentTimeMillis();
try {
ConcurrentMap<String, String> map = cache.asMap();
//新增一个刷新缓存锁,这个boolean具有原子操作 线程安全,开始刷新缓存时,我们需要把锁开启,结束后关闭锁,期间不允许调用缓存即可,这个过程很快所以无需担心获取缓存慢
boolean lock = REFRESH_CACHING.compareAndSet(false, true);
if (lock) {
map.keySet().forEach(item -> {
if (item.equals(key)){
map.remove(item);
}
});
REFRESH_CACHING.set(false);
} else {
log.error("正在刷新,请等待");
}
} catch (Exception e) {
log.error("刷新缓存异常", e);
//这里刷新缓存失败,保证锁释放不影响缓存调用
REFRESH_CACHING.set(false);
}
log.info("刷新缓存耗时:{}", System.currentTimeMillis() - start);
}
.接下来开始验证
我们先把 refreshCache2() 删除缓存的过程中 新增一段测试代码
故意延长一下缓存刷新的时间,然后在刷新缓存的过程中 我们去获取缓存
/**
* 删除缓存-刷新
*/
public void refreshCache2() {
long start = System.currentTimeMillis();
try {
ConcurrentMap<String, String> map = cache.asMap();
//新增一个刷新缓存锁,这个boolean具有原子操作 线程安全,开始刷新缓存时,我们需要把锁开启,结束后关闭锁,期间不允许调用缓存即可,这个过程很快所以无需担心获取缓存慢
boolean lock = REFRESH_CACHING.compareAndSet(false, true);
if (lock) {
map.keySet().forEach(item -> map.remove(item));
Thread.sleep(3000);
REFRESH_CACHING.set(false);
} else {
log.error("正在刷新,请等待");
}
} catch (Exception e) {
log.error("刷新缓存异常", e);
//这里刷新缓存失败,保证锁释放不影响缓存调用
REFRESH_CACHING.set(false);
}
log.info("刷新缓存耗时:{}", System.currentTimeMillis() - start);
}
然后写两个测试入口,方便调用缓存方法
@Slf4j
@RestController
@RequestMapping("/cacheTest")
public class CacheTestController {
@GetMapping("/getCache")
public String getCache() {
try {
String testCache = TestCache.getTestCache("1");
return testCache;
} catch (Exception e) {
log.error("获取缓存异常");
return "error";
}
}
@PostMapping("/refresh")
public int refresh() {
try {
TestCache.refreshCache2();
return 1;
} catch (Exception e) {
log.error("刷新缓存异常");
return 0;
}
}
}
1 我们先调用 /getCache 看一下获取缓存方法正常不
可以正常拿到缓存,这是首次拿缓存 默认的key 是"1" 看一下控制台输出
可以看到这里从load这里获取的数据 uuid ,重复请求获取key="1"缓存直接从缓存中取值,并没有走load方法,
下面测试刷新缓存时获取缓存,看下锁是否生效, 我们现请求 /refresh 然后接着请求 /getCache
结果ok
好了测试结束,感谢大家阅读,如果发现问题请大佬指出,谢谢