GuavaCache中解决业务NPE 问题

1,007 阅读3分钟

前段时间线上会报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,但是这种情况发生的概率很低,即使发生了,也是不符合预期的。