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;
    }

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM