背景

今天在使用的时候使用GuavaCache的refreshAfterWrite的功能时,发现在少数场景下会报错CacheLoader returned null for key。但是如果把refreshAfterWrite去掉时,又不会报错。具体错误内容是这样的。

com.google.common.cache.CacheLoader$InvalidCacheLoadException: CacheLoader returned null for key ValueOfKeyIsNull.at com.google.common.cache.LocalCache$Segment.getAndRecordStats(LocalCache.java:2348)at com.google.common.cache.LocalCache$Segment.loadSync(LocalCache.java:2318)at com.google.common.cache.LocalCache$Segment.lockedGetOrLoad(LocalCache.java:2280)at com.google.common.cache.LocalCache$Segment.get(LocalCache.java:2195)at com.google.common.cache.LocalCache.get(LocalCache.java:3934)at com.google.common.cache.LocalCache.getOrLoad(LocalCache.java:3938)at com.google.common.cache.LocalCache$LocalLoadingCache.get(LocalCache.java:4821)at com.google.guava.cache.GuavaRefreshWhenCacheIsNullTest.testGuavaRefreshWhenCacheIsNullThrowsException(GuavaRefreshWhenCacheIsNullTest.java:49)

探寻

首先为什么如果不用refreshAfterWrite功能时为什么不会有问题?由于好奇,只能去源码里查找答案。基于报错内容,在com.google.common.cache.LocalCache.Segment#getAndRecordStats找到这一段源代码

        value = getUninterruptibly(newValue);if (value == null) {throw new InvalidCacheLoadException("CacheLoader returned null for key " + key + ".");}

大致意思是从ListenableFuture newValue这个Future中获取到的值不能为空,如果为空,则直接报一个InvalidCacheLoadException异常。

当我们使用了refreshAfterWrite功能时,必须build一个自己实现的CacheLoader,这时会返回一个com.google.common.cache.LocalCache.LocalLoadingCache的LoadingCache实例。从org.springframework.cache.guava.GuavaCache代码中,发现这么一段代码

 @Overridepublic ValueWrapper get(Object key) {if (this.cache instanceof LoadingCache) {try {Object value = ((LoadingCache<Object, Object>) this.cache).get(key);return toValueWrapper(value);}catch (ExecutionException ex) {throw new UncheckedExecutionException(ex.getMessage(), ex);}}return super.get(key);}

当这个cache是LoadingCache时,走的获取key对应的value的方式是不同的。依次会走到com.google.common.cache.LocalCacheSegment.loadSync,然后到com.google.common.cache.LocalCacheSegment.loadSync,然后到com.google.common.cache.LocalCacheSegment.loadSync,然后到com.google.common.cache.LocalCacheSegment.getAndRecordStats,最终获取的value如果为null的话,则直接报错,即使你在GuavaCacheManager层面设置了setAllowNullValues(true)也依然会报错。

分析

如果不是LoadingCache的话,那是允许返回null值的,且不会报错。但是使用了refreshAfterWrite功能后,是不允许的。其实仔细想一想也是很合理的,这里我们重写了CacheLoader,CacheLoader的一个重要的工作就是在2次获取同一个key时,且key到了该refresh的时间,就会后台异步刷新,如果刷新这个key得到了新值,就会覆盖key对应的旧值。但是如果得到了null,应该怎么做呢?刷新还是不管?GuavaCache表示自己也很无奈,干脆报错,让业务层自己去理会好了。

不过,个人觉得这种方式还是比较粗暴。就算是使用了refreshAfterWrite,也不敢保证自己的每个key都能对应值。但是从报错位置的代码来看,确实没有可设置的参数给业务来屏蔽这个异常。

处理方法1:异常捕捉

有一种最挫最简单的方法,在get的时候catch住异常,异常情况下直接返回null,这种方法简单粗暴又有效

处理方法2:使用Optional

对于null值的处理,java8是提供了一种很好的处理方法,就是Optional类。对value值统一使用Optional封装,业务方拿到Optional时,通过Optional.orElse(null)方法拿到真实值,避免在CacheLoader的load中返回null。关于Optional,更多详细内容可以参考我的另一篇博客Java8新特性学习(二)- Optional类。

下面代码已上传到 github - common-caches

@Testpublic void testGuavaRefreshWhenCacheIsNullReturnNull() {CacheBuilder<Object, Object> cacheBuilder = CacheBuilder.newBuilder().refreshAfterWrite(10, TimeUnit.SECONDS).expireAfterWrite(20, TimeUnit.SECONDS);LoadingCache<String, Optional<String>> refreshWarehouseCache = cacheBuilder.build(new CacheLoader<String, Optional<String>>() {@Overridepublic Optional<String> load(String key) {if ("ValueOfKeyIsNull".equals(key)) {return Optional.empty();}return Optional.of("1234567890");}@Overridepublic ListenableFuture<Optional<String>> reload(String key, Optional<String> oldValue) {System.out.println("testGuavaRefresh reload : key=" + key);return Futures.immediateFuture(load(key));}});try {Optional<String> myValue = refreshWarehouseCache.get("myKey");Assert.assertEquals("1234567890", myValue.orElse(null));myValue = refreshWarehouseCache.get("ValueOfKeyIsNull");//get myValue is nullAssert.assertNull(myValue.orElse(null));} catch (ExecutionException e) {e.printStackTrace();}}

处理方法3:使用特殊值标记null值

这是找一个特殊的值,且不会在真实环境中不会有和这个特殊值相同。这里以value是String类型为例,当然如果是Object类型的,也是可以判断的,只要XXXObject某些关键字段的值不一样就行,可以使用Objects.equals()来判定是否是特殊值,主要要重写这个XXXObject的equals和hashCode方法就行了。

下面代码已上传到 github - common-caches

    @Testpublic void testGuavaRefreshWhenCacheIsNullReturnDefaultNullValue() {CacheBuilder<Object, Object> cacheBuilder = CacheBuilder.newBuilder().refreshAfterWrite(10, TimeUnit.SECONDS).expireAfterWrite(20, TimeUnit.SECONDS);String nullValue = "nullValue";LoadingCache<String, String> refreshWarehouseCache = cacheBuilder.build(new CacheLoader<String, String>() {@Overridepublic String load(String key) {if ("ValueOfKeyIsNull".equals(key)) {return nullValue;}return "1234567890";}@Overridepublic ListenableFuture<String> reload(String key, String oldValue) {System.out.println("testGuavaRefresh reload : key=" + key);return Futures.immediateFuture(load(key));}});try {String myValue = refreshWarehouseCache.get("myKey");Assert.assertEquals("1234567890", myValue);//throws ExceptionmyValue = refreshWarehouseCache.get("ValueOfKeyIsNull");Assert.assertEquals(nullValue, myValue);} catch (ExecutionException e) {e.printStackTrace();}}

总结

前面的博客有讲过GuavaCache相关的内容,包括缓存篇(一)- Guava 和 Guava Cache expireAfterWrite 与 refreshAfterWrite区别.

关于GuavaCache,其实有一些设计比较好的方面,但是也存在一些可以完善的方面。在使用的过程中,不断发现设计好的学习过来。你觉得还有哪些设计不好的方面,欢迎一起交流。

我先来一个觉得不好的吧。spring中集成的Guava Cache,一个GuavaCacheManager,只设计了一个CacheLoader,但是cacheName却有多个,这就意味着一个CacheName在后台异步刷新时,需要考虑多个不同的cacheName的情况。而CacheLoader中只能通过Object key来判断当前这个key是属于哪个cacheName的,进而再调用对应的cacheName的刷新方法去刷新,这是比较困难的一件事,如果你的多个cacheName的key是没有什么特别的规则的话,这简直就是一个灾难。

CacheLoader returned null for key分析和解决相关推荐

  1. collect2.exe: error: ld returned 1 exit status分析与解决

    阅读前请看一下:我是一个热衷于记录的人,每次写博客会反复研读,尽量不断提升博客质量.文章设置为仅粉丝可见,是因为写博客确实花了不少精力.希望互相进步谢谢!! 文章目录 阅读前请看一下:我是一个热衷于记 ...

  2. SpringBoot集成Redis报non null key required(已解决)

    SpringBoot集成Redis报non null key required(已解决) 注意是不是给的key值或传过来的值是不是空的,如果是空的就会报这个错 接下来将流程走下去: 1.创建Sprin ...

  3. Redis 热 Key 发现以及解决办法

    背景介绍 最近在技术交流微信群里看大家讨论技术,其中有谈到 Redis 热 Key 的一些问题解决方案,我也仔细思考了一下我们目前系统中 Redis 的使用场景,我们是不是也存在热 Key 问题,或者 ...

  4. 递归算法造成的问题分析与解决

    原文是在我自己博客中,小伙伴也可以点阅读原文进行跳转查看,还有好听的背景音乐噢~ 递归,在编码中应该算是一种很常见的算法了.之前在学习C语言的时候,也同样了解过一些基本的算法,比如斐波那契.在学习的时 ...

  5. redis一般用来干嘛_谈谈redis的热key问题如何解决

    公众号:孤独烟 ,作者:孤独烟 引言 今天我们来写redis方面的内容,谈谈热key问题如何解决. 其实热key问题说来也很简单,就是瞬间有几十万的请求去访问redis上某个固定的key,从而压垮缓存 ...

  6. mysql添加外键1215错误_MySQL添加外键时报错:1215 Cannot add the foreign key constraint的解决方法...

    前言 这篇文章主要涉及到在数据创建表时,遇到ERROR 1215 (HY000): Cannot add foreign key constraint 问题方面的内容,对于在数据创建表时,遇到同样问题 ...

  7. mysql数据库连接过多的错误,可能的原因分析及解决办法

    mysql数据库连接过多的错误,可能的原因分析及解决办法 来源:网络采集 作者:未知 系统不能连接数据库,关键要看两个数据: 1.数据库系统允许的最大可连接数max_connections.这个参数是 ...

  8. Discuz升级 Database Error : pre_common_syscache ADD PRIMARY KEY (cname)【解决办法】

    错误码: 1068 Multiple primary key defined Execution Time : 00:00:00:000 Transfer Time : 00:00:00:000 To ...

  9. not null primary key什么意思_explain都不会用,你还好意思说精通Mysql查询优化?

    Explain简介 Explain关键字是Mysql中sql优化的常用「关键字」,通常都会使用Explain来「查看sql的执行计划,而不用执行sql」,从而快速的找出sql的问题所在. 在讲解Exp ...

最新文章

  1. HBase在大搜车金融业务中的应用实践
  2. 实验四 [bx]和loop的使用+段前缀
  3. 推荐一套高效的码字工具
  4. html转excel有问题,html转excel
  5. 【转】从零开始学图形学:10分钟看懂贝塞尔曲线
  6. vue抽屉_VUE组件 之 Drawer 抽屉
  7. 洛谷P2851 [USACO06DEC]最少的硬币The Fewest Coins(完全背包+多重背包)
  8. 多层数组如何遍历_带你从零学大数据系列之Java篇---第五章:数组
  9. leetcode刷题日记-1044. 最长重复子串
  10. LaTex问题解决集[2]:解决插入Visio图片有多余边框和白边的问题
  11. 计算机连接网络显示有限访问权限,电脑WiFi连接总是提示有限的访问权限是什么意思...
  12. 微信公众号 返回40163
  13. PostMan 调用 Auth2.0 获取Token 报错问题总结
  14. 数字小人时钟windows电脑屏幕保护
  15. 第三章 概念模型设计(一)
  16. adjacent_find
  17. 新品发布季第二场,APT威胁挖掘机「NDR流量监测系统」正式亮相
  18. 用Photoshop将照片卡通化
  19. 机器学习之过拟合和欠拟合
  20. opencv进行简单的裂缝检测

热门文章

  1. 关键字深度剖析,集齐所有关键字可召唤神龙?【三】
  2. 从王自如和老罗的论战中我貌似懂得了点神马...
  3. win10无限蓝屏_Win10升级系统后蓝屏或无限重启解决办法
  4. TYVJ 木瓜地
  5. 【附资料】PMP证书有用吗?
  6. UI设计中置灰功能总结
  7. 解决WINDOWS邮箱无法收取QQ邮件
  8. java.lang.ArrayStoreException
  9. Android底部菜单栏(图片+文字)
  10. 支持P2P传输的M3U8播放器