【问题标题】:RxJava multiple consumers of one publisherRxJava 一个发布者的多个消费者
【发布时间】:2020-06-16 09:25:48
【问题描述】:

我正在编写某种带有缓存的中间件 HTTP 代理。工作流程是:

  1. 客户端向此代理请求资源
  2. 如果缓存中存在资源,则代理返回它
  3. 如果未找到资源,则代理获取远程资源并返回给用户。代理在数据加载时将此资源保存到缓存中。

我的接口有用于远程资源的Publisher<ByteBuffer> 流,接受Publisher<ByteBuffer> 保存的缓存,以及接受Publisher<ByteBuffer> 作为响应的客户端连接:

// remote resource
interface Resource {
  Publisher<ByteBuffer> fetch();
}

// cache
interface Cache {
  Completable save(Publisher<ByteBuffer> data);
}

// clien response connection
interface Connection {
  Completable send(Publisher<ByteBuffer> data);
}

我的问题是我需要在向客户端发送响应时延迟保存这个字节缓冲区流以缓存,因此客户端应该负责从远程资源请求ByteByffer块, 不缓存。

我尝试使用Publisher::cache 方法,但这对我来说不是一个好的选择,因为它将所有接收到的数据保存在内存中,这是不可接受的,因为缓存的数据可能只有几 GB 大小。

作为一种解决方法,我创建了Subject,并由来自Resource 的下一个项目填充:

private final Cache cache;
private final Connection out;

Completable proxy(Resource res) {
  Subject<ByteBuffer> mirror = PublishSUbject.create();
  return Completable.mergeArray(
    out.send(res.fetch().doOnNext(mirror::onNext),
    cache.save(mirror.toFlowable(BackpressureStrategy.BUFFER))
  );
}

是否可以重复使用相同的 Publisher 而无需在内存中缓存项目,并且只有一个订阅者负责从发布者请求项目?

【问题讨论】:

  • 我听不懂你说:所以客户端应该负责从远程资源请求ByteByffer块,而不是缓存。!
  • @bubbles 我的意思是背压:Publisher 的Subscription 具有request(long n) 方法,它正在从Publisher 请求下一个n 的项目数量。客户端的Connection 比Cache 慢,所以只有Connection 应该负责从远程Publisher 的Resource 请求下一个n 数量的ByteBuffer
  • 这是什么版本的 RxJava?我在 2.x 上,Publisher 只有一种方法:Publisher.subscribe(Subscriber&lt;? super T&gt;)

标签: java caching rx-java reactive-streams backpressure


【解决方案1】:

我可能遗漏了一些东西(添加了关于我的 Publisher 界面版本不同的评论)。

但是..这就是我在概念上会如何做这样的事情。

我将简化处理Integers的接口:

// remote resource
interface Resource {
  ConnectableObservable<Integer> fetch();
}

// cache
interface Cache {
  Completable save(Integer data);
}

// client response connection
interface Connection {
  Completable send(Integer data);
}

我会使用Observable::publish 创建一个ConnectableObservable 并建立两个订阅:

@Test
public void testProxy()
{
    // Override schedulers:
    TestScheduler s = new TestScheduler();
    
    RxJavaPlugins.setIoSchedulerHandler(
            scheduler -> s );
    RxJavaPlugins.setComputationSchedulerHandler(
            scheduler -> s );
    
    // Mock interfaces:
    Resource resource = () -> Observable.range( 1, 100 )
            .publish();
    
    Cache cache = data -> Completable.fromObservable( Observable.just( data )
                .delay( 100, TimeUnit.MILLISECONDS )
                .doOnNext( __ -> System.out.println( String.format( "Caching %d", data ))));
    
    Connection connection = data -> Completable.fromObservable( Observable.just( data )
                .delay( 500, TimeUnit.MILLISECONDS )
                .doOnNext( __ -> System.out.println( String.format( "Sending %d", data ))));
    
    // Subscribe to resource:
    ConnectableObservable<Integer> observable = resource.fetch();
    
    observable
        .observeOn( Schedulers.io() )
        .concatMapCompletable( data -> connection.send( data ))
        .subscribe();
    
    observable
        .observeOn( Schedulers.computation() )
        .concatMapCompletable( data -> cache.save( data ))
        .subscribe();
    
    observable.connect();
    
    // Simulate passage of time:
    s.advanceTimeBy( 10, TimeUnit.SECONDS );
}

输出:

Caching 1
Caching 2
Caching 3
Caching 4
Sending 1
Caching 5
Caching 6
Caching 7
Caching 8
Caching 9
Sending 2
Caching 10
. . . 

更新

根据您的 cmets,听起来尊重背压对您来说很重要。

假设您在某处有一个支持背压的Publisher,您可以将其转换为Flowable,如下所示:

Flowable<T> flowable = Flowable.fromPublisher( publisher );

一旦您拥有Flowable,您就可以允许多个订阅者,而不必担心每个订阅者都必须从Publisher 请求值(或者任一订阅者在建立订阅时会丢失任何事件)。你可以通过调用flowable.publish() 来创建ConnectableFlowable。

ConnectableFlowable<T> flowable = Flowable.fromPublisher( publisher ).publish();
out.send(flowable);   // calls flowable.subscribe()
cache.save(flowable); // calls flowable.subscribe()
flowable.connect();   // begins emitting values

【讨论】:

  • 感谢您的回答。我正在使用 RxJava2,Publicher 接口是反应流的一部分1.0.0,请参阅reactive-streams.org/reactive-streams-1.0.0-javadoc/org/… 实际上与您的接口存在一些不匹配,因为Cache 和Connection 接受Publisher&lt;T&gt;(让我们使用@987654345 @ 而不是整数和缓冲区),而不是普通整数。所以问题是Subscriber&lt;T&gt;s 和Publisher 都可能在onSubscribe() 调用之后从Subscription 请求下一个项目
  • @Kirill 在您的链接中,Publisher 有一种方法:Publisher::subscribe。您的问题引用了 Publisher::cache 方法,而您的代码似乎引用了 Publisher::doOnNext。这两个我都找不到。
  • 在我对问题的原始评论中,我告诉了Publisher 的Subscription,而不是Publisher 本身,它有方法request(long):reactive-streams.org/reactive-streams-1.0.0-javadoc/org/… 另外,我在问题中提到过背压是很重要的一部分,Cache 作为消费者要快得多,所以它会填满ConnectableFlowable 的缓冲区,并且在发送到慢Connection 之前内存会充满项目,这是主要问题(见起源问题)。
  • @Kirill ConnectableFlowable 尊重来自最慢消费者的背压,您可以通过调用 Flowable.publish(int bufferSize) 来控制其缓冲区的大小。假设您的底层资源/发布者也尊重背压,那么应该没有内存消耗问题。
  • 谢谢,我刚刚证实,可连接的发布者尊重来自最慢消费者的背压。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-06-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多