guava cache大量的WARN日志的问题分析
一、问题显现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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需要刷新了,过期了,或者主动触发刷新值了。
1 2 3 4 5 6 7 8 | 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的代码那边
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | /** * 缓存队列变量 */ 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()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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
1 2 3 4 5 6 7 8 | /** * ClassName NullObject * Author shenjing * Date 2019/7/10 * Version 1.0 **/ public class NullObject { } |
3. 在通用的getCacheByName的方法中进行判断,取到的对象是不是NullObject类型的,如果是,则返回null给外层,进行重新加载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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; } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!