• guava cache大量的WARN日志的问题分析


    一、问题显现

    2019-04-21 11:16:32 [http-nio-4081-exec-2] WARN  com.google.common.cache.LocalCache - Exception thrown during refresh
    com.google.common.cache.CacheLoader$InvalidCacheLoadException: CacheLoader returned null for key BKCIYear0.
    	at com.google.common.cache.LocalCache$Segment.getAndRecordStats(LocalCache.java:2350)
    	at com.google.common.cache.LocalCache$Segment$1.run(LocalCache.java:2331)
    	at com.google.common.util.concurrent.MoreExecutors$DirectExecutor.execute(MoreExecutors.java:457)
    	at com.google.common.util.concurrent.ExecutionList.executeListener(ExecutionList.java:156)
    	at com.google.common.util.concurrent.ExecutionList.add(ExecutionList.java:101)
    	at com.google.common.util.concurrent.AbstractFuture.addListener(AbstractFuture.java:170)
    	at com.google.common.cache.LocalCache$Segment.loadAsync(LocalCache.java:2326)
    	at com.google.common.cache.LocalCache$Segment.refresh(LocalCache.java:2389)
    	at com.google.common.cache.LocalCache$Segment.scheduleRefresh(LocalCache.java:2367)
    	at com.google.common.cache.LocalCache$Segment.get(LocalCache.java:2187)
    	at com.google.common.cache.LocalCache.get(LocalCache.java:3937)
    	at com.google.common.cache.LocalCache.getOrLoad(LocalCache.java:3941)
    	at com.google.common.cache.LocalCache$LocalLoadingCache.get(LocalCache.java:4824)
    	at com.kcidea.sushibase.Service.Cache.GoogleLocalCache.getCacheByName(GoogleLocalCache.java:42)  

    google的这个开发工具里面的缓存是个轻量化的缓存,类似一个HashMap的实现,google在里面加了很多同步异步的操作。使用起来简单,不用额外搭建redis服务,故项目中使用了这个缓存。

    有一天生产环境直接假死了,赶紧上服务器排查,发现日志里面有大量的报WARN错误,只要触发cache的get就会报警告,由于cache的触发频率超高,导致了日志磁盘爆满,一天好几个G的日志里面全是WARN的错误。但是在开发环境下根本不触发这个错误,怎么调试都没有进这段代码里面。先暂时停用了缓存,然后开始排查。

      二、问题排查

    1. 根据报错的堆栈,一点一点往上找,直到找到这一行的时候发现了一些端倪,他想找一个newValue

    at com.google.common.cache.LocalCache$Segment.refresh(LocalCache.java:2389)

    2. 继续顺着这条线往里面找,直到找到这段代码,为什么要找newValue呢,map需要刷新了,过期了,或者主动触发刷新值了。

      if (map.refreshes()
              && (now - entry.getWriteTime() > map.refreshNanos)
              && !entry.getValueReference().isLoading()) {
            V newValue = refresh(key, hash, loader, true);
            if (newValue != null) {
              return newValue;
            }
          }
    

     3. 然后就可以解释问题为什么只在生产环境出现,而开发环境不出现了,因为是触发了过期时间,我们设置的过期时间是30分钟,所以开发环境很少调试超过30分钟的,每次都是重新运行,所以根本触发不到这个超时的地方。

    4. 然后接着调试,发现会走到我们一开始初始化cache的代码那边

        /**
         * 缓存队列变量
         */
        static LoadingCache<String, Object> cache = CacheBuilder.newBuilder()
                // 给定时间内没有被读/写访问,则回收。
                .refreshAfterWrite(CACHE_OUT_TIME, TimeUnit.MINUTES)
                // 缓存过期时间和redis缓存时长一样
                .expireAfterAccess(CACHE_OUT_TIME, TimeUnit.MINUTES)
                // 设置缓存个数
                .maximumSize(50000).
                        build(new CacheLoader<String, Object>() {
                            @Override
                            public Object load(String key) throws Exception {
                                //找不到就返回null (1)
                                return null;
                            }
                        });
    

     注意上面的代码,(1)的位置,找不到就返回null,在网上找的代码里面这里通常写的是return null或者return doThingsTheHardWay(key)之类的,但是没有详细的doThingsTheHardWay描述,所以我这里写了个null。

    所以根本的问题就是这里返回null导致的错误了。

    三、解决方案

    找到了问题原因,解决方案就相对来说容易的很多了

    1. 修改(1)处的代码,将return null修改成return new NullObject()  

        static LoadingCache<String, Object> cache = CacheBuilder.newBuilder()
                // 给定时间内没有被读/写访问,则回收。
                .refreshAfterWrite(CACHE_OUT_TIME, TimeUnit.MINUTES)
                // 缓存过期时间和redis缓存时长一样
                .expireAfterAccess(CACHE_OUT_TIME, TimeUnit.MINUTES)
                // 设置缓存个数
                .maximumSize(50000).
                        build(new CacheLoader<String, Object>() {
    
                            @Override
                            public Object load(String key) throws Exception {
                                //尝试将这里改成new NullObject,外面进行判断
                                return new NullObject();
                            }
                        });
    

      

    2. 定义一个空白的类就叫NullObject

    /**
     * ClassName   NullObject
     * Author      shenjing
     * Date        2019/7/10
     * Version     1.0
     **/
    public class NullObject {
    }
    

      

    3. 在通用的getCacheByName的方法中进行判断,取到的对象是不是NullObject类型的,如果是,则返回null给外层,进行重新加载。

      private static <T> T getCacheByName(String name) {
            T ret = null;
            try {
                if (cache.asMap().containsKey(name)) {
                    ret = (T) cache.get(name);
                    if (ret.getClass().equals(NullObject.class)) {
                        //缓存已过期,返回null
                        return null;
                    }
                    log.debug("缓存读取[{}]成功", name);
                }
            } catch (Exception ex) {
                log.debug("缓存[{}]读取失败:{}", name, ex.getMessage());
            }
    
            return ret;
        }
    

      

  • 相关阅读:
    浩然战法--黄金柱选股
    《含泪活着》主人公一一丁尚彪,叙述在日本当黑户口的危险经历,美国《世界日报》2017年3月16日连载
    Java8与JDK8和JDK1.8有什么区别?
    ThreadLocal
    什么是jsonp
    Java中比较对象大小的两种实现方式
    MySQL教程之concat以及group_concat的用法
    多级树形结构和sql查询实现
    mysql树形结构递归查询
    Maven配置教程
  • 原文地址:https://www.cnblogs.com/JangoJing/p/11162459.html
Copyright © 2020-2023  润新知