一、問題顯現
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; }