CacheLoader returned null for key分析和解决

背景

今天在使用的时候使用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<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.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,更多详细内容可以参考我的另一篇博客Java8新特性学习(二)- Optional类

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


    @Test
    public 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>>() {
            @Override
            public Optional<String> load(String key) {
                if ("ValueOfKeyIsNull".equals(key)) {
                    return Optional.empty();
                }
                return Optional.of("1234567890");
            }

            @Override
            public 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 null
            Assert.assertNull(myValue.orElse(null));
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

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

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

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

    @Test
    public 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>() {
            @Override
            public String load(String key) {
                if ("ValueOfKeyIsNull".equals(key)) {
                    return nullValue;
                }
                return "1234567890";
            }

            @Override
            public 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 Exception
            myValue = refreshWarehouseCache.get("ValueOfKeyIsNull");
            Assert.assertEquals(nullValue, myValue);
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

总结

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

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

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


版权声明:本文为codingtu原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。