前段时间线上会报NullPointerException,追溯到代码,发现是我们依赖的服务挂了,我们在GuavaCache的load方法中调用了该服务,导致cache重新加载后的缓存为null,而这个null会影响后面的业务,问题找到了,那怎么解决呢?先来看代码:
public LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(CACHE_MAXIMUN_SIZE)
.refreshAfterWrite(REFRESH_DURATION, TimeUnit.MINUTES)
.build(new CacheLoader<String, String>() {
@Override
public String load(String s) throws ExecutionException {
//这个就是我们依赖的服务,也就是去平台上获取配置
String config = configGateway.getConfig(s);
return config;
}
});
上面是cache的配置,当configGateway.getConfig()服务停掉之后,那我们获取到的config就为null,导致cache里面原始的正常的旧值被覆盖,而下面的代码是我们调用cache的过程。
@Override
public Result<List<String>> getConfig() {
List<String> res = null;
Map<String, Integer> map;
try {
String config = cache.get(KEY);
map = GSON.fromJson(config, new TypeToken<Map<String, Integer>>() {
}.getType());
res = new ArrayList<>(map.keySet());
} catch (ExecutionException e) {
log.error("error:",e);
}
return Result.success(res);
}
可以看到,当config为null时,map通过json转换也为null,而map.keySet()就会报NPE,也许有的人说初始化一下map就不会报NPE,但是这并没有解决根本问题,因为这样返回的res为null,业务上的需求是要正常显示配置,显示null也不满足业务需求。所以我们想如何才能在依赖服务崩溃的时候用cache的旧值(前提:平台上的config更新不频繁),至少让它在app上正常显示,于是。。。
public LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(CACHE_MAXIMUN_SIZE)
.refreshAfterWrite(REFRESH_DURATION, TimeUnit.MINUTES)
.build(new CacheLoader<String, String>() {
@Override
public String load(String s) throws ExecutionException {
String config = configGateway.getConfig(s);
//当依赖服务崩溃时,我们调用返回旧值去覆盖
if (Objects.isNull(config)) {
return cache.get(s);
}
return config;
}
});
我们很简单的加了一步判断,当依赖服务崩溃时,我们返回cache中的旧值,让旧值去覆盖旧值,我们get的时候就会获取到正常的数据,也可以避免后面的NPE。
这时候有人会问,极端情况下,当我们服务刚起起来的时候,依赖的服务就是宕机的状态,那一开始load就需要去get,而此时cache本身就是null,那get会出现死循环的问题吗?先看看源码:
public V get(K key) throws ExecutionException {
return this.localCache.getOrLoad(key);
}
V getOrLoad(K key) throws ExecutionException {
return this.get(key, this.defaultLoader);
}
V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {
int hash = this.hash(Preconditions.checkNotNull(key));
return this.segmentFor(hash).get(key, hash, loader);
}
V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
Preconditions.checkNotNull(key);
Preconditions.checkNotNull(loader);
try {
//第一次进来,count为0,跳过if
if (this.count != 0) {
LocalCache.ReferenceEntry<K, V> e = this.getEntry(key, hash);
if (e != null) {
long now = this.map.ticker.read();
V value = this.getLiveValue(e, now);
if (value != null) {
this.recordRead(e, now);
this.statsCounter.recordHits(1);
Object var17 = this.scheduleRefresh(e, key, hash, value, now, loader);
return var17;
}
LocalCache.ValueReference<K, V> valueReference = e.getValueReference();
if (valueReference.isLoading()) {
Object var9 = this.waitForLoadingValue(e, key, valueReference);
return var9;
}
}
}
//首次调用会执行lockedGetOrLoad函数
Object var15 = this.lockedGetOrLoad(key, hash, loader);
return var15;
} catch (ExecutionException var13) {
Throwable cause = var13.getCause();
if (cause instanceof Error) {
throw new ExecutionError((Error)cause);
} else if (cause instanceof RuntimeException) {
throw new UncheckedExecutionException(cause);
} else {
throw var13;
}
} finally {
this.postReadCleanup();
}
}
V lockedGetOrLoad(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
LocalCache.ValueReference<K, V> valueReference = null;
LocalCache.LoadingValueReference<K, V> loadingValueReference = null;
boolean createNewEntry = true;
this.lock();
LocalCache.ReferenceEntry e;
try {
long now = this.map.ticker.read();
this.preWriteCleanup(now);
int newCount = this.count - 1;
AtomicReferenceArray<LocalCache.ReferenceEntry<K, V>> table = this.table;
int index = hash & table.length() - 1;
LocalCache.ReferenceEntry<K, V> first = (LocalCache.ReferenceEntry)table.get(index);
//e为null,跳过for循环
for(e = first; e != null; e = e.getNext()) {
K entryKey = e.getKey();
if (e.getHash() == hash && entryKey != null && this.map.keyEquivalence.equivalent(key, entryKey)) {
valueReference = e.getValueReference();
if (valueReference.isLoading()) {
createNewEntry = false;
} else {
V value = valueReference.get();
if (value == null) {
this.enqueueNotification(entryKey, hash, value, valueReference.getWeight(), RemovalCause.COLLECTED);
} else {
if (!this.map.isExpired(e, now)) {
this.recordLockedRead(e, now);
this.statsCounter.recordHits(1);
Object var16 = value;
return var16;
}
this.enqueueNotification(entryKey, hash, value, valueReference.getWeight(), RemovalCause.EXPIRED);
}
this.writeQueue.remove(e);
this.accessQueue.remove(e);
this.count = newCount;
}
break;
}
}
if (createNewEntry) {
loadingValueReference = new LocalCache.LoadingValueReference();
if (e == null) {
//先new一个newEntry
e = this.newEntry(key, hash, first);
e.setValueReference(loadingValueReference);
table.set(index, e);
} else {
e.setValueReference(loadingValueReference);
}
}
} finally {
this.unlock();
this.postWriteCleanup();
}
if (createNewEntry) {
Object var9;
try {
//调用loadSync进行数据load
synchronized(e) {
var9 = this.loadSync(key, hash, loadingValueReference, loader);
}
} finally {
this.statsCounter.recordMisses(1);
}
return var9;
} else {
return this.waitForLoadingValue(e, key, valueReference);
}
}
V getAndRecordStats(K key, int hash, LocalCache.LoadingValueReference<K, V> loadingValueReference, ListenableFuture<V> newValue) throws ExecutionException {
Object value = null;
Object var6;
try {
value = Uninterruptibles.getUninterruptibly(newValue);
if (value == null) {
throw new InvalidCacheLoadException("CacheLoader returned null for key " + key + ".");
}
this.statsCounter.recordLoadSuccess(loadingValueReference.elapsedNanos());
this.storeLoadedValue(key, hash, loadingValueReference, value);
var6 = value;
} finally {
if (value == null) {
this.statsCounter.recordLoadException(loadingValueReference.elapsedNanos());
//cache抛java.lang.IllegalStateException后会对缓存中的entry进行remove操作
this.removeLoadingValue(key, hash, loadingValueReference);
}
}
return var6;
}
读源码会发现在lockedGetOrLoad中会先newEntry后面才load,在这种情况下只需要抛出异常信息就行,这样LocalLoadingCache会放弃缓存,执行removeLoadingValue将load异常后的key删除。
当然,这样虽然不会死循环,但是获取到的依旧为null,但是这种情况发生的概率很低,即使发生了,也是不符合预期的。