如何使用反应式编程实现嵌套的异步代码?

时间:2019-04-26 05:28:04

标签: java asynchronous reactive-programming project-reactor

我对反应式编程非常陌生。尽管我对函数式编程和Kotlin协程非常熟悉,但是我仍然无法弄清楚如何使用反应式编程范例来重构普通的嵌套CRUD代码,尤其是那些具有嵌套异步操作的代码。

例如,以下是基于Java 8 CompletableFuture

的简单异步CRUD代码段

        getFooAsync(id)
                .thenAccept(foo -> {
                    if (foo == null) {
                        insertFooAsync(id, new Foo());
                    } else {
                        getBarAsync(foo.bar)
                                .thenAccept(bar -> {
                                   updateBarAsync(foo, bar);
                                });
                    }
                });

使用kotlin协程进行重构非常容易,这使得它在不失去异步性的情况下更具可读性。

 val foo = suspendGetFoo(id)
 if(foo==null) {
   suspendInsertFoo(id, Foo())
 } else {
   val bar = suspendGetBar(foo.bar)
   suspendUpdateBar(foo, bar);-
}

但是,类似的代码是否适合于反应式编程?

如果是这样,给定Flux<String> idFlux,如何使用Reactor 3对其进行重构?

只用CompletableFuture替换每个Mono是个好主意吗?

1 个答案:

答案 0 :(得分:7)

  

像这样的代码适合于反应式编程吗?

恕我直言,Kotlin协程更适合此用例,并导致更简洁的代码。

但是,您可以在响应流中执行此操作。

  

只用Mono替换每个CompletableFuture是一个好主意吗?

我发现反应式流可以很好地处理许多异步用例(例如examples from project reactor)。但是,肯定有一些用例不太合适。因此,我不建议使用反应性流替换每个 CompletableFuture的政策。

但是,当您需要背压时,您必须离开CompletableFuture的一种情况。

要使用哪种异步模式,很多决定取决于您使用的语言/框架/工具/库以及您和您的团队成员对它们的适应程度。如果您正在使用具有Kotlin良好支持的库,并且您的团队熟悉Kotlin,请使用协程。对于反应流也是如此。

  

给定Flux<String> idFlux,如何使用Reactor 3对其进行重构?

在考虑此用例的反应流时,需要牢记以下几点:

  1. 反应性流无法发出null。相反,通常使用空的Mono。 (从技术上讲,您也可以使用Mono<Optional<...>>,但那时候您只是在伤脑筋,乞求bug)
  2. Mono为空时,lambda传递给任何处理onNext信号的运算符(例如.map.flatMap.handle等) 不被调用。请记住,您正在处理的是数据流(而不是命令性控制流)
  3. .switchIfEmpty.defaultIfEmpty运算符可以在空的Mono上进行操作。但是,它们不提供else条件。下游操作员不知道该流以前是空的(除非从Publisher传递到.switchIfEmpty的元素很容易识别)
  4. 如果流中包含多个运算符,并且多个运算符可能导致该流为空,那么下游操作员很难/不可能确定该流为何为空。
  5. 允许处理上游运算符发出的值的主要异步运算符为.flatMap.flatMapSequential.concatMap。您将需要使用它们来链接对先前异步操作的输出进行操作的异步操作。
  6. 由于您的用例未返回一个值,因此反应流实现将返回一个Mono<Void>

话虽如此,这是尝试将您的示例转换为3号反应堆(有一些警告)的尝试:

    Mono<Void> updateFoos(Flux<String> idFlux) {
        return idFlux                                         // Flux<String>
            .flatMap(id -> getFoo(id)                         // Mono<Foo>
                /*
                 * If a Foo with the given id is not found,
                 * create a new one, and continue the stream with it.
                 */
                .switchIfEmpty(insertFoo(id, new Foo()))      // Mono<Foo>
                /*
                 * Note that this is not an "else" condition
                 * to the above .switchIfEmpty
                 *
                 * The lambda passed to .flatMap will be
                 * executed with either:
                 * A) The foo found from getFoo
                 *    OR
                 * B) the newly inserted Foo from insertFoo
                 */
                .flatMap(foo -> getBar(foo.bar)               // Mono<Bar>
                    .flatMap(bar -> updateBar(foo, bar))      // Mono<Bar>
                    .then()                                   // Mono<Void>
                )                                             // Mono<Void>
            )                                                 // Flux<Void>
            .then();                                          // Mono<Void>
    }

    /*
     * @return the Foo with the given id, or empty if not found
     */
    abstract Mono<Foo> getFoo(String id);

    /*
     * @return the Bar with the given id, or empty if not found
     */
    abstract Mono<Bar> getBar(String id);

    /*
     * @return the Foo inserted, never empty
     */
    abstract Mono<Foo> insertFoo(String id, Foo foo);

    /*
     * @return the Bar updated, never empty
     */
    abstract Mono<Bar> updateBar(Foo foo, Bar bar);

这是一个更复杂的示例,该示例使用Tuple2<Foo,Boolean>指示是否找到了原始的Foo(这在语义上应等同于您的示例):

    Mono<Void> updateFoos(Flux<String> idFlux) {
        return idFlux                                         // Flux<String>
            .flatMap(id -> getFoo(id)                         // Mono<Foo>
                /*
                 * Map to a Tuple2 whose t2 indicates whether the foo was found.
                 * In this case, it was found.
                 */
                .map(foo -> Tuples.of(foo, true))             // Mono<Tuple2<Foo,Boolean>>
                /*
                 * If a Foo with the given id is not found,
                 * create a new one, and continue the stream with 
                 * a Tuple2 indicating it wasn't originally found
                 */
                .switchIfEmpty(insertFoo(id, new Foo())       // Mono<Foo>
                    /*
                     * Foo was not originally found, so t2=false
                     */
                    .map(foo -> Tuples.of(foo, false)))       // Mono<Tuple2<Foo,Boolean>>
                /*
                 * The lambda passed to .flatMap will be
                 * executed with either:
                 * A) t1=foo found from getFoo, t2=true
                 *    OR
                 * B) t1=newly inserted Foo from insertFoo, t2=false
                 */
                .flatMap(tuple2 -> tuple2.getT2()
                    // foo originally found 
                    ? getBar(tuple2.getT1().bar)              // Mono<Bar>
                        .flatMap(bar -> updateBar(tuple2.getT1(), bar)) // Mono<Bar>
                        .then()                               // Mono<Void>
                    // foo originally not found (new inserted)
                    : Mono.empty()                            // Mono<Void>
                )
            )                                                 // Flux<Void>
            .then();                                          // Mono<Void>
    }