RxJava模式,用于请求具有临时缓存的远程Observable

时间:2017-03-31 23:35:10

标签: java android caching rx-java

用例是这样的: 我想暂时缓存最新发出的昂贵的Observable响应,但在它过期后,返回昂贵的源Observable并再次缓存它等等。

一个非常基本的网络缓存方案,但我真的很难让它运行起来。

private Observable<String> getContentObservable() {

    // expensive upstream source (API, etc.)
    Observable<String> sourceObservable = getSourceObservable();

    // cache 1 result for 30 seconds, then return to the source
    return sourceObservable
            .replay(1, 30, TimeUnit.SECONDS)
            .autoConnect()
            .switchIfEmpty(sourceObservable);
}

初始请求:转到源代码 源发出后30秒内的第二个请求:从缓存传递 缓存到期窗口之外的第三个请求:没有。我订阅它并且我没有获得数据,但它没有切换到上游源Observable。

看起来好像我只是从autoConnect()连接到我的ConnectableObservable而且它永远不会以空的方式完成,因此它永远不会触发我的switchIfEmpty()

如何使用replay(1,x,x)switchIfEmpty()的这种组合?

或者我从一开始就接近这个错误?

3 个答案:

答案 0 :(得分:3)

return sourceObservable
            .replay(1, 30, TimeUnit.SECONDS)
            .autoConnect()
            .switchIfEmpty(sourceObservable);
     

初始请求:在源发出后30秒内转到源第二个请求:从缓存传递缓存到期窗口之外的第三个请求:没有。我订阅它并且我没有获得数据,但它没有切换到上游源Observable。

这里的问题是,重播只是重复过去30秒内sourceObservable发出的相同序列,但是当你在30秒后订阅时,序列没有事件,甚至没有onCompleted() ,所以你不能switchIfEmpty(),它不会起作用,因为它取决于'onCompleted()'信号,没有任何排放,要知道它是'空的'。

通常,在缓存方案中使用重放是不够的,因为您需要的是在缓存过期的情况下再次重新订阅的方法,并且另外按需要执行,这意味着仅在某些客户端订阅它时。 (你可以做每100秒刷新一次的缓存,但这不是我想要的行为)

因此,正如@Yurly Kulikov建议的那样,您需要维护一个状态,并控制订阅操作以维持状态。 但是我认为解决方案中有一个主要的流程,因为它实际上并不是完全线程安全的,这意味着如果2在另一个之后订阅它,比如说A和B,而A执行请求并等待以保存新的结果是缓存,B也可以订阅,另一个请求将被执行,因为A尚未设置缓存值(它还没有完成第一个网络请求。

我建议在不同的实现中使用类似的方法,我建议here

public class CachedRequest<T> {

private final AtomicBoolean expired = new AtomicBoolean(true);
private final Observable<T> source;
private final long cacheExpirationInterval;
private final TimeUnit cacheExpirationUnit;
private Observable<T> current;

    public CachedRequest(Observable<T> o, long cacheExpirationInterval,
                         TimeUnit cacheExpirationUnit) {
        source = o;
        current = o;
        this.cacheExpirationInterval = cacheExpirationInterval;
        this.cacheExpirationUnit = cacheExpirationUnit;
   }

    private Observable<T> getCachedObservable() {
        return Observable.defer(() -> {
            if (expired.compareAndSet(true, false)) {
                current = source.cache();
                Observable.timer(cacheExpirationInterval, cacheExpirationUnit)                          
                        .subscribe(aLong -> expired.set(true));
            }
            return current;
        });
    }
}

with defer你可以根据缓存过期状态返回正确的Observable,因此每个订阅发生在缓存过期内都会被缓存Observable(使用cache()) - 意味着请求只会执行一次。在缓存过期后,附加订阅将触发新请求,并将设置新计时器以重置缓存过期。

答案 1 :(得分:1)

您必须维护许多呼叫者之间共享的状态。这就是为什么每次调用getContentObservable()时都无法创建Observable。

一种方法是在外部创建一个Observable,在Observable中保持内部状态(例如使用缓冲区),但是如果没有Observable,实现有状态行为通常会更容易。

以下是字段中共享状态的示例:

private Optional<String> cached = Optional.empty();

private Observable<String> getContentObservable() {
    //use defer to delay cache evaluation to the point when someone subscribes
    return Observable.defer(
        () ->
            cached.isPresent()
                ? Observable.just(cached)
                : fetchAndCache()
    )
    //use the same scheduler for every cached field access
    .subscribeOn(scheduler);
}

private Observable<String> fetchAndCache() {
    Observable<String> cachedSource = getSourceObservable()
        //I assume you only need one, make sure it is 1
        .take(1)
        .cache();

    cachedSource
        .observeOn(scheduler)
        //side-effect stores the state
        .doOnNext(str -> cached = Optional.of(str))
        .flatMap(str -> Observable.timer(30, TimeUnit.SECONDS, scheduler))
        //another side-effect clears the cache
        .subscribe(l -> cached = Optional.empty());

    return cachedSource;        
}

答案 2 :(得分:1)

事实证明,即使在处置完毕后,您也可以使用Jake Wharton的重播份额来缓存最后一个值。 https://github.com/JakeWharton/RxReplayingShare