响应式编程 - 入门介绍

概念

与传统编程模型对比

传统编程模型,主要特点是同步阻塞式-Blocking; 而响应式编程(Reactive Programming) 主要特点是异步非阻塞 Non-Blocking。

同步阻塞式参考下面的图:
同步阻塞
同步阻塞的方式很好理解,也容易实现,在大部分时候都是不错的选择。但它的问题是当并发量高的时候,会有资源的浪费,遇到性能瓶颈。而我们也希望能够最大程度地利用硬件资源,利用多核CPU提高服务的一个吞吐量,所以有了异步非阻塞的编程模式。

响应式数据流异步非阻塞参考下图:

异步非阻塞
无论是查询数据库还是Web端过来的请求,都被看做一个一个的事件,而这些事件组成了一个事件流。对于这些事件,我们注册相关的订阅者去响应它,从而从编程的角度上实现异步。

这其中涉及到几个关键字:响应式数据流事件驱动发布订阅模式

Java异步非阻塞实现

Java提供了两种方式来实现异步编程:

  • Callback 使用回调,例如一个lambda或者一个匿名类作为参数,当异步执行有结果后进行调用
  • Future/CompletableFuture 立刻返回一个Future<T>对象。

但是这两种方式都有各自的缺陷。
😟 Callback的问题

很难组合,容易形成嵌套,甚至导致“回调地狱”,会导致代码的可读性和可维护性都较差。

🌰 举个栗子, 我们需要通过userId获取该用户最喜欢的5个UI,如果该用户没有设置喜欢的UI,就返回建议给他。这里有三个服务,getFavorites(),getSuggestions(),getDetails()分别获取用户喜欢的UI的id,获取给用户的建议,获取UI的具体信息。

userService.getFavorites(userId, new Callback<List<String>>() { 
  public void onSuccess(List<String> list) { 
    if (list.isEmpty()) { 
      suggestionService.getSuggestions(new Callback<List<Favorite>>() {
        public void onSuccess(List<Favorite> list) { 
          UiUtils.submitOnUiThread(() -> { 
            list.stream()
                .limit(5)
                .forEach(uiList::show); 
            });
        }

        public void onError(Throwable error) { 
          UiUtils.errorPopup(error);
        }
      });
    } else {
      list.stream() 
          .limit(5)
          .forEach(favId -> favoriteService.getDetails(favId, 
            new Callback<Favorite>() {
              public void onSuccess(Favorite details) {
                UiUtils.submitOnUiThread(() -> uiList.show(details));
              }

              public void onError(Throwable error) {
                UiUtils.errorPopup(error);
              }
            }
          ));
    }
  }

  public void onError(Throwable error) {
    UiUtils.errorPopup(error);
  }
});

而响应式编程,以project reactor框架为例,只需要以下代码实现:

userService.getFavorites(userId) 
           .flatMap(favoriteService::getDetails) 
           .switchIfEmpty(suggestionService.getSuggestions()) 
           .take(5) 
           .publishOn(UiUtils.uiThreadScheduler()) 
           .subscribe(uiList::show, UiUtils::errorPopup);

😟 Future的问题
Future要比Callback好一些,即使Java 8引入了CompletableFuture,这些API仍然存在一些问题:

  • 如果使用了get()方法,仍然会让你的实现最后沦为阻塞式
  • 不支持lazy computation
  • 对多个值处理和异常处理等支持较少

🌰 举个栗子

CompletableFuture<List<String>> ids = ifhIds(); 

CompletableFuture<List<String>> result = ids.thenComposeAsync(l -> { 
	Stream<CompletableFuture<String>> zip =
			l.stream().map(i -> { 
				CompletableFuture<String> nameTask = ifhName(i); 
				CompletableFuture<Integer> statTask = ifhStat(i); 

				return nameTask.thenCombineAsync(statTask, (name, stat) -> "Name " + name + " has stats " + stat); 
			});
	List<CompletableFuture<String>> combinationList = zip.collect(Collectors.toList()); 
	CompletableFuture<String>[] combinationArray = combinationList.toArray(new CompletableFuture[combinationList.size()]);

	CompletableFuture<Void> allDone = CompletableFuture.allOf(combinationArray); 
	return allDone.thenApply(v -> combinationList.stream()
			.map(CompletableFuture::join) 
			.collect(Collectors.toList()));
});

List<String> results = result.join(); 
assertThat(results).contains(
		"Name NameJoe has stats 103",
		"Name NameBart has stats 104",
		"Name NameHenry has stats 105",
		"Name NameNicole has stats 106",
		"Name NameABSLAJNFOAJNFOANFANSF has stats 121");

应用reactor的操作符可以写为:

Flux<String> ids = ifhrIds(); 

Flux<String> combinations =
		ids.flatMap(id -> { 
			Mono<String> nameTask = ifhrName(id); 
			Mono<Integer> statTask = ifhrStat(id); 

			return nameTask.zipWith(statTask, 
					(name, stat) -> "Name " + name + " has stats " + stat);
		});

Mono<List<String>> result = combinations.collectList(); 

List<String> results = result.block(); 
assertThat(results).containsExactly( 
		"Name NameJoe has stats 103",
		"Name NameBart has stats 104",
		"Name NameHenry has stats 105",
		"Name NameNicole has stats 106",
		"Name NameABSLAJNFOAJNFOANFANSF has stats 121"
);

Project Reactor

如今有很多优秀的响应式编程框架,如RxJava, Project Reactor, Vert.x, Akka, Spring的WebFlux等。上面举例子的时候也用了Project Reactor来做说明。

关于Project Reactor, 有两个基本的组件需要了解,一个是Mono,一个是Flux

Mono

Mono可以理解为发射一个事件的发布平台。有很多静态工厂方法可以创建一个Mono,如Mono.just("foo") Mono参考文档
Mono

🌰举个栗子,假如我需要在一个水果loaderCache更新方法里,实现异步,延迟更新里面的水果图片对象,可以这样做:

private FruitLoader updateFruitLoader(Integer key) {
	//...
    Mono.delay(Duration.ofSeconds(1)).subscribeOn(scheduler).subscribe(t -> {
        FruitLoader loader = fruitLoaderCache.get(key);
        if (loader != null) {
            Optional.ofNullable(loader.loadFruit()).ifPresent(fruitType -> {
                fruitPictureCache.refresh(fruitType);
            });
        }
    });
    //...
}

Flux

Flux可以理解为可以发射N个事件的发布平台,同样有很多静态工厂方法可以构建:Flux参考文档
Flux示意图

//todo

参考资料:
https://time.geekbang.org/dailylesson/detail/100075754
https://time.geekbang.org/dailylesson/detail/100056946
https://tech.io/playgrounds/929/reactive-programming-with-reactor-3/Flux
https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html
https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Mono.html
https://www.cnblogs.com/littleatp/p/11515458.html
https://blog.csdn.net/get_set/article/details/79576874
https://projectreactor.io/docs/core/release/reference/#getting-started
https://www.cnblogs.com/cjsblog/p/12580518.html
https://juejin.cn/post/6844904193946451981

posted @ 2022-06-09 19:15  rachel_aoao  阅读(980)  评论(0编辑  收藏  举报