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.LocalCacheSegment.loadSynccom.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


    @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是沒有什麼特別的規則的話,這簡直就是一個災難。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章