美文网首页
Guava Cache本地缓存使用小结

Guava Cache本地缓存使用小结

作者: 因你而在_caiyq | 来源:发表于2021-02-01 15:50 被阅读0次

    原创文章,转载请注明原文章地址,谢谢!

    Guava Cache介绍

    guava cache是google guava中的一个内存缓存模块,用于将数据缓存到JVM内存中,实际项目开发中经常将一些比较公共或者常用的数据缓存起来方便快速访问。

    内存缓存最常见的就是基于HashMap实现的缓存,为了解决并发问题也可能也会用到ConcurrentHashMap等并发集合,但是内存缓存需要考虑很多问题,包括并发问题、缓存过期机制、缓存移除机制、缓存命中统计率等。guava cache已经考虑到这些问题,可以上手即用。通过CacheBuilder创建缓存、然后设置缓存的相关参数、设置缓存的加载方法等,在多线程高并发场景中往往是离不开cache的,需要根据不同的应用场景来需要选择不同的cache,比如分布式缓存如Redis、memcached,还有本地(进程内)缓存如ehcache、GuavaCache。Guava Cache与ConcurrentMap很相似,但也不完全一样。最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除。相对地,Guava Cache为了限制内存占用,通常都设定为自动回收元素。

    初始化
    final static Cache<Integer, String> cache = CacheBuilder.newBuilder()  
            //设置cache的初始大小为10,要合理设置该值  
            .initialCapacity(10)  
            //设置并发数为5,即同一时间最多只能有5个线程往cache执行写入操作  
            .concurrencyLevel(5)  
            //设置cache中的数据在写入之后的存活时间为10秒  
            .expireAfterWrite(10, TimeUnit.SECONDS)  
            //构建cache实例  
            .build(); 
    // 放入缓存
    // cache.put("key", "value"); 
    // 获取缓存
    // String value = cache.getIfPresent("key");
    
    缓存回收
    • 基于容量的回收
      如果要规定缓存项的数目不超过固定值,只需使用CacheBuilder.maximumSize(long)。缓存将尝试回收最近没有使用或总体上很少使用的缓存项。
    LoadingCache<String, Object> caches = CacheBuilder.newBuilder()
        .maximumSize(100)
        .build(new CacheLoader<String, Object>() {
            @Override
            public Object load(String key) throws Exception {
                return null;
            }
        });
    

    注意:在缓存项的数目达到限定值之前,缓存就可能进行回收操作。通常来说,这种情况发生在缓存项的数目逼近限定值时。

    • 定时回收
    LoadingCache<String, Object> caches = CacheBuilder.newBuilder()
        .maximumSize(100)
        /*
        缓存项在给定时间内没有被读/写访问,则回收。
        请注意这种缓存的回收顺序和基于大小回收一样。
         */
        .expireAfterAccess(10, TimeUnit.SECONDS)
        /*
        缓存项在给定时间内没有被写访问(创建或覆盖),则回收。
        如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。
         */
        .expireAfterWrite(10, TimeUnit.SECONDS)
        .build(new CacheLoader<String, Object>() {
            @Override
            public Object load(String key) throws Exception {
                return null;
            }
        });
    
    • 基于引用的回收
    LoadingCache<String, Object> caches = CacheBuilder.newBuilder()
        .maximumSize(100)
        /*
        使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。
        因为垃圾回收仅依赖恒等式(==),使用弱引用键的缓存用==而不是equals比较键。
         */
        .weakKeys()
        /*
        使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收。
        因为垃圾回收仅依赖恒等式(==),使用弱引用值的缓存用==而不是equals比较值。
         */
        .weakValues()
        /*
        使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。
        考虑到使用软引用的性能影响,通常建议使用更有性能预测性的缓存大小限定。
        使用软引用值的缓存同样用==而不是equals比较值。
         */
        .softValues()
        .build(new CacheLoader<String, Object>() {
            @Override
            public Object load(String key) throws Exception {
                return null;
            }
        });
    
    • 显式清除

    任何时候,都可以显式地清除缓存项,而不是等到它被回收。

    String key = "demo_key";
    List<String> keys = Arrays.asList("key1", "key2", "key3");
    LoadingCache<String, Object> caches = CacheBuilder.newBuilder()
        .maximumSize(100)
        .build(new CacheLoader<String, Object>() {
            @Override
            public Object load(String key) throws Exception {
                return null;
            }
        });
    //个别清除
    caches.invalidate(key);
    //批量清除
    caches.invalidateAll(keys);
    //清除所有缓存项
    caches.invalidateAll();
    
    • 移除监听器
    LoadingCache<String, Object> caches = CacheBuilder.newBuilder()
        .maximumSize(100)
        /*
        通过CacheBuilder.removalListener(RemovalListener),可以声明一个监听器,以便缓存项被移除时做一些额外操作。
        缓存项被移除时,RemovalListener会获取移除通知,其中包含移除原因、键和值。
         */
        .removalListener(new RemovalListener<Object, Object>() {
            @Override
            public void onRemoval(RemovalNotification<Object, Object> removalNotification) {
                log.info("移除缓存,key:{}", removalNotification.getKey());
            }
        })
        .build(new CacheLoader<String, Object>() {
            @Override
            public Object load(String key) throws Exception {
                return null;
            }
        });
    
    • 清理什么时候发生?

    使用CacheBuilder构建的缓存不会"自动"执行清理和回收工作,也不会在某个缓存项过期后马上清理,也没有诸如此类的清理机制。相反,它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做(如果写操作实在太少的话)。这样做的原因在于,如果要自动地持续清理缓存,就必须有一个线程,这个线程会和用户操作竞争共享锁。此外,某些环境下线程创建可能受限制,这样CacheBuilder就不可用了。相反,把选择权交到你手里。如果你的缓存是高吞吐的,那就无需担心缓存的维护和清理等工作。如果你的缓存只会偶尔有写操作,而你又不想清理工作阻碍了读操作,那么可以创建自己的维护线程,以固定的时间间隔调用Cache.cleanUp()。

    LoadingCache<String, Object> caches = CacheBuilder.newBuilder()
        .maximumSize(100)
        .build(new CacheLoader<String, Object>() {
            @Override
            public Object load(String key) throws Exception {
                return null;
            }
        });
    caches.cleanUp();
    

    Guava Cache简单使用

    定时过期
    • 问题:如果用其他的缓存方式,如redis,使用“如果有缓存则返回;否则运算、缓存、然后返回”的缓存模式是有很大弊端的。当高并发条件下同时进行get操作,而此时缓存值已过期时,会导致大量线程都调用生成缓存值的方法,比如从数据库读取。这时候就容易造成大量请求同时查询数据库中该条记录,也就是“缓存击穿”。

    • 解决办法:Guava cache对此种情况有一定控制。当大量线程用相同的key获取缓存值时,只会有一个线程进入load方法,而其他线程则等待,直到缓存值被生成。这样也就避免了缓存击穿的危险。

    • 代码:新建一个变量为caches的一个缓存对象,maximumSize定义了缓存的容量大小,当缓存数量即将到达容量上线时,则会进行缓存回收,回收最近没有使用或总体上很少使用的缓存项。需要注意的是在接近这个容量上限时就会发生,所以在定义这个值的时候需要视情况适量地增大一点。另外通过expireAfterWrite这个方法定义了缓存的过期时间,写入十分钟之后过期。在build方法里,传入了一个CacheLoader对象,重写了其中的load方法。当获取的缓存值不存在或已过期时,则会调用此load方法,进行缓存值的计算。

    @Test
    public void testExpire() throws Exception {
        LoadingCache<String, Object> caches = CacheBuilder.newBuilder()
            .maximumSize(100)
            .expireAfterWrite(10, TimeUnit.SECONDS)
            .build(new CacheLoader<String, Object>() {
                @Override
                public Object load(String key) throws Exception {
                    log.info("重新加载缓存,key:{}", key);
                    return generateValueByKey(key);
                }
            });
        Long key = 1L;
        log.info("获取缓存数据,key:{},value:{}", key, caches.get(String.valueOf(key)));
        TimeUnit.SECONDS.sleep(5);
        log.info("5秒钟后取缓存,key:{},value:{}", key, caches.get(String.valueOf(key)));
        TimeUnit.SECONDS.sleep(10);
        log.info("再过10秒钟后取缓存,key:{},value:{}", key, caches.get(String.valueOf(key)));
    }
    
    定时刷新
    • 问题:没有了缓存击穿的情况,但是每当某个缓存值过期时,也会导致大量的请求线程被阻塞。

    • 解决办法:Guava提供了另一种缓存策略,缓存值定时刷新,更新线程调用load方法更新该缓存,其他请求线程返回该缓存的旧值。这样对于某个key的缓存来说,只会有一个线程被阻塞,用来生成缓存值,而其他的线程都返回旧的缓存值,不会被阻塞。

    • 代码:这里的定时并不是真正意义上的定时。Guava cache的刷新需要依靠用户请求线程,让该线程去进行load方法的调用,所以如果一直没有用户尝试获取该缓存值,则该缓存也并不会刷新。

    @Test
    public void testRefresh() throws Exception {
        LoadingCache<String, Object> caches = CacheBuilder.newBuilder()
            .maximumSize(100)
            .refreshAfterWrite(10, TimeUnit.SECONDS)
            .build(new CacheLoader<String, Object>() {
                @Override
                public Object load(String key) throws Exception {
                    log.info("重新加载缓存,key:{}", key);
                    return generateValueByKey(key);
                }
            });
        Long key = 1L;
        log.info("获取缓存数据,key:{},value:{}", key, caches.get(String.valueOf(key)));
        TimeUnit.SECONDS.sleep(5);
        log.info("5秒钟后取缓存,key:{},value:{}", key, caches.get(String.valueOf(key)));
        TimeUnit.SECONDS.sleep(10);
        log.info("再过10秒钟后取缓存,key:{},value:{}", key, caches.get(String.valueOf(key)));
        TimeUnit.SECONDS.sleep(100);
    }
    
    异步刷新
    • 问题:当缓存的key很多时,高并发条件下大量线程同时获取不同key对应的缓存,此时依然会造成大量线程阻塞,并且给数据库带来很大压力。

    • 解决办法:将刷新缓存值的任务交给后台线程,所有的用户请求线程均返回旧的缓存值,这样就不会有用户线程被阻塞了。

    • 代码:新建了一个线程池,用来执行缓存刷新任务,并且重写了CacheLoader的reload方法,在该方法中建立缓存刷新的任务并提交到线程池。注意此时缓存的刷新依然需要靠用户线程来驱动,只不过和上述方法不同之处在于该用户线程触发刷新操作之后,会立马返回旧的缓存值。

    @Test
    public void testAsyncRefresh() throws Exception {
        ListeningExecutorService asyncRefreshPools = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(20));
        LoadingCache<String, Object> caches = CacheBuilder.newBuilder()
            .maximumSize(100)
            .refreshAfterWrite(10, TimeUnit.SECONDS)
            .build(new CacheLoader<String, Object>() {
                @Override
                public Object load(String key) throws Exception {
                    log.info("重新加载缓存,key:{}", key);
                    return generateValueByKey(key);
                }
    
                @Override
                public ListenableFuture<Object> reload(String key, Object oldValue) throws Exception {
                    return asyncRefreshPools.submit(new Callable<Object>() {
                        @Override
                        public Object call() throws Exception {
                            log.info("线程池提交任务刷新缓存,key:{}", key);
                            return generateValueByKey(key);
                        }
                    });
                }
            });
        Long key = 1L;
        log.info("获取缓存数据,key:{},value:{}", key, caches.get(String.valueOf(key)));
        TimeUnit.SECONDS.sleep(5);
        log.info("5秒钟后取缓存,key:{},value:{}", key, caches.get(String.valueOf(key)));
        TimeUnit.SECONDS.sleep(10);
        log.info("再过10秒钟后取缓存,key:{},value:{}", key, caches.get(String.valueOf(key)));
        TimeUnit.SECONDS.sleep(100);
    }
    
    工具类封装
    • 简单封装1
    @Slf4j
    public class LocalCacheUtils<K, V> {
    
        private static LocalCacheUtils ourInstance = new LocalCacheUtils();
        private LoadingCache<K, V> cache;
    
        public static LocalCacheUtils getInstance() {
            return ourInstance;
        }
    
        private LocalCacheUtils() {
            if (cache == null) {
                cache = CacheBuilder.newBuilder()
                    //最大容量,超过最大值后按照LRU最近最少使用算法来移除缓存项
                    .maximumSize(10000)
                    //初始化容量
                    .initialCapacity(100)
                    //30秒没有被读/写访问,则回收
                    .expireAfterWrite(30, TimeUnit.SECONDS)
                    //1分钟后为缓存增加自动定时刷新功能
                    .refreshAfterWrite(5, TimeUnit.SECONDS)
                    //使用软引用存储值
                    .softValues()
                    //开启统计功能
                    .recordStats()
                    //并发级别为5,并发级别是指可以同时写缓存的线程数
                    .concurrencyLevel(5)
                    //移除通知
                    .removalListener((RemovalListener<K, V>) removalListener ->
                        log.info("removalListener - > localKey为{}", removalListener.getKey()))
                    .build(new CacheLoader<K, V>() {
                        @Override
                        public V load(K key) throws Exception {
                            return null;
                        }
                    });
            }
        }
    
        public void set(K key, V value) {
            log.info("本地缓存set操作 key为{} value为{}", key, value);
            cache.put(key, value);
        }
    
        public V get(K key, Callable<V> callable) {
            try {
                log.info("本地缓存get操作 key为{}", key);
                return cache.get(key, callable);
            } catch (Exception e) {
                log.error("本地缓存异常", e);
            }
            return null;
        }
    }
    
    • 简单封装2
    @Slf4j
    public abstract class BaseGuavaCache<K, V> {
    
        /*** 缓存自动刷新周期 */
        private int refreshDuration = 10;
    
        /*** 缓存刷新周期时间格式 */
        private TimeUnit refreshTimeUnit = TimeUnit.MINUTES;
    
        /*** 缓存过期时间(可选择) */
        private int expireDuration = -1;
    
        /*** 缓存刷新周期时间格式 */
        private TimeUnit expireTimeUnit = TimeUnit.HOURS;
    
        /*** 缓存最大容量 */
        private int maxSize = 4;
    
        /*** 数据刷新线程池 */
        private static ListeningExecutorService refreshPool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(20));
    
        private LoadingCache<K, V> cache = null;
    
        /**
         * 用于初始化缓存值(某些场景下使用,例如系统启动检测缓存加载是否征程)
         */
        public abstract void loadValueWhenStarted();
    
        /**
         * 定义缓存值的计算方法,新值计算失败时抛出异常,get操作时将继续返回旧的缓存
         *
         * @param key 缓存key
         * @return 缓存value
         * @throws Exception 异常
         */
        public abstract V getValueWhenExpired(K key) throws Exception;
    
        /**
         * 从cache中拿出数据操作
         *
         * @param key 缓存key
         * @return 缓存value
         * @throws Exception 异常
         */
        public V getValue(K key) throws Exception {
            try {
                return getCache().get(key);
            } catch (Exception e) {
                log.error("从内存缓存中获取内容时发生异常,key: " + key, e);
                throw e;
            }
        }
    
        public V getValueOrDefault(K key, V defaultValue) {
            try {
                return getCache().get(key);
            } catch (Exception e) {
                log.error("从内存缓存中获取内容时发生异常,key: " + key, e);
                return defaultValue;
            }
        }
    
        public BaseGuavaCache<K, V> setRefreshDuration(int refreshDuration) {
            this.refreshDuration = refreshDuration;
            return this;
        }
    
        public BaseGuavaCache<K, V> setRefreshTimeUnit(TimeUnit refreshTimeUnit) {
            this.refreshTimeUnit = refreshTimeUnit;
            return this;
        }
    
        public BaseGuavaCache<K, V> setExpireDuration(int expireDuration) {
            this.expireDuration = expireDuration;
            return this;
        }
    
        public BaseGuavaCache<K, V> setExpireTimeUnit(TimeUnit expireTimeUnit) {
            this.expireTimeUnit = expireTimeUnit;
            return this;
        }
    
        public BaseGuavaCache<K, V> setMaxSize(int maxSize) {
            this.maxSize = maxSize;
            return this;
        }
    
        public void clearAll() {
            this.getCache().invalidateAll();
        }
    
        /**
         * 获取cache实例
         *
         * @return 缓存实例
         */
        private LoadingCache<K, V> getCache() {
            if (cache == null) {
                synchronized (this) {
                    if (cache == null) {
                        CacheBuilder<Object, Object> cacheBuilder = CacheBuilder.newBuilder().maximumSize(maxSize);
                        if (refreshDuration > 0) {
                            cacheBuilder.refreshAfterWrite(refreshDuration, refreshTimeUnit);
                        }
                        if (expireDuration > 0) {
                            cacheBuilder.expireAfterWrite(expireDuration, expireTimeUnit);
                        }
                        cache = cacheBuilder.build(new CacheLoader<K, V>() {
                            @Override
                            public V load(K key) throws Exception {
                                return getValueWhenExpired(key);
                            }
    
                            @Override
                            public ListenableFuture<V> reload(final K key, V oldValue) throws Exception {
                                return refreshPool.submit(new Callable<V>() {
                                    @Override
                                    public V call() throws Exception {
                                        return getValueWhenExpired(key);
                                    }
                                });
                            }
                        });
                    }
                }
            }
            return cache;
        }
    
        @Override
        public String toString() {
            return "GuavaCache";
        }
    }
    

    博客内容仅供自已学习以及学习过程的记录,如有侵权,请联系我删除,谢谢!

    相关文章

      网友评论

          本文标题:Guava Cache本地缓存使用小结

          本文链接:https://www.haomeiwen.com/subject/zsnjtltx.html