一, 问题显现
- 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;
- }
来源: https://www.cnblogs.com/JangoJing/p/11162459.html