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 

@Resource
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() 删除缓存的过程中 新增一段测试代码

Thread.sleep(3000); 

故意延长一下缓存刷新的时间,然后在刷新缓存的过程中 我们去获取缓存

/**
     * 删除缓存-刷新
     */
    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

 

好了测试结束,感谢大家阅读,如果发现问题请大佬指出,谢谢

posted @ 2023-10-27 17:59  loveCrane  阅读(1192)  评论(0编辑  收藏  举报