背景
今天在使用的時候使用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.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相關的內容,包括緩存篇(一)- Guava 和 Guava Cache expireAfterWrite 與 refreshAfterWrite區別.
關於GuavaCache,其實有一些設計比較好的方面,但是也存在一些可以完善的方面。在使用的過程中,不斷髮現設計好的學習過來。你覺得還有哪些設計不好的方面,歡迎一起交流。
我先來一個覺得不好的吧。spring中集成的Guava Cache,一個GuavaCacheManager,只設計了一個CacheLoader,但是cacheName卻有多個,這就意味着一個CacheName在後臺異步刷新時,需要考慮多個不同的cacheName的情況。而CacheLoader中只能通過Object key來判斷當前這個key是屬於哪個cacheName的,進而再調用對應的cacheName的刷新方法去刷新,這是比較困難的一件事,如果你的多個cacheName的key是沒有什麼特別的規則的話,這簡直就是一個災難。