博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
CacheLoader returned null for key分析和解决
阅读量:2442 次
发布时间:2019-05-10

本文共 5934 字,大约阅读时间需要 19 分钟。

背景

今天在使用的时候使用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代码中,发现这么一段代码

@Override	public ValueWrapper get(Object key) {
if (this.cache instanceof LoadingCache) {
try {
Object value = ((LoadingCache
) 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.LocalCache S e g m e n t . l o a d S y n c , 然 后 到 c o m . g o o g l e . c o m m o n . c a c h e . L o c a l C a c h e Segment.loadSync,然后到com.google.common.cache.LocalCache Segment.loadSynccom.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,更多详细内容可以参考我的另一篇博客。

下面代码已上传到

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

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

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

下面代码已上传到

@Test    public void testGuavaRefreshWhenCacheIsNullReturnDefaultNullValue() {        CacheBuilder
cacheBuilder = CacheBuilder.newBuilder() .refreshAfterWrite(10, TimeUnit.SECONDS) .expireAfterWrite(20, TimeUnit.SECONDS); String nullValue = "nullValue"; LoadingCache
refreshWarehouseCache = cacheBuilder.build(new CacheLoader
() { @Override public String load(String key) { if ("ValueOfKeyIsNull".equals(key)) { return nullValue; } return "1234567890"; } @Override public ListenableFuture
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 Exception myValue = refreshWarehouseCache.get("ValueOfKeyIsNull"); Assert.assertEquals(nullValue, myValue); } catch (ExecutionException e) { e.printStackTrace(); } }

总结

前面的博客有讲过GuavaCache相关的内容,包括 和 .

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

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

转载地址:http://dhnqb.baihongyu.com/

你可能感兴趣的文章
共享软件中注册部分的简单实现(转)
查看>>
RedHat Linux 9下所有权和许可权限(转)
查看>>
C++程序设计从零开始之语句(转)
查看>>
利用Apache+PHP3+MySQL建立数据库驱动的动态网站(转)
查看>>
C#中实现DataGrid双向排序(转)
查看>>
利用C语言小程序来解决大问题(转)
查看>>
简单方法在C#中取得汉字的拼音的首字母(转)
查看>>
编程秘籍:使C语言高效的四大绝招(转)
查看>>
计算机加锁 把U盘变成打开电脑的钥匙(转)
查看>>
Fedora Core 4 基础教程 (上传完毕)(转)
查看>>
删除MSSQL危险存储过程的代码(转)
查看>>
红旗软件:树立国际的Linux品牌(转)
查看>>
Linux学习要点(转)
查看>>
影响mysqld安全的几个选项(转)
查看>>
最新版本Linux Flash 9 Beta开放发布(转)
查看>>
mysql事务处理(转)
查看>>
Fedora 显示设备配置工具介绍(转)
查看>>
FREEBSD 升级及优化全攻略(转)
查看>>
系统移民须知:Linux操作系统安装要点(转)
查看>>
在redhat系统中使用LVM(转)
查看>>