响应式(Reactor)编程简介

1,006 阅读4分钟

Reactor简介

Reactor是一个支持背压(backpressure)的实现了Reactive Streams规范的运行在JVM上的非阻塞反应式编程的类库。

为什么要使用Reactor

互联网下的高并发问题

在现代互联网的架构下,解决高并发问题的方式大致有两个方向

  • 并行化(parallelize ),使用多线程提高硬件资源的利用率。
  • 异步化,尽可能的提高现在资源的利用率。

并行化的解决方案

现在多线程似乎已经成为了解决并发问题的标准模式。通常我们使用阻塞代码来编写程序,当出现性能瓶颈的时候,我们可以引入额外的线程来运行阻塞的代码, 但是这种方式很快会带来争用和并发问题,并且被阻塞的线程资源处在空闲状态而被浪费,所以并行化并不是解决问题的灵丹妙药。

  • 线程上下文的切换比较耗时(system call)。
  • 线程本身就是一种比较宝贵的资源。
  • 需要解决线程的同步问题。

异步化的解决方案

Java提供了两种异步化的API,CallbackFuture。但是这两种技术都有他们的局限性。

  • Callback很难组合在一起,代码的维护和阅读都比较困难。一旦陷入回调地狱(Callback Hell)代码将会变得无法维护。
  • Java8中的CompletableFuture虽然对Future进行了增强,但是在事件编排能力依旧表现不佳。
    • future.get()依然是阻塞的。
    • 不能支持惰性计算(lazy computation)。
    • 对异常的高级处理支持比较弱。

回调地狱的例子

一个需求:

  1. 通过用户ID获取获取用户的收藏夹,并将前五个收藏夹的信息展示到用户的UI上,如果收藏夹为空,则展示5个建议。
// 1.通过userId获取收藏夹
userService.getFavorites(userId, new Callback<List<String>>() { 
  public void onSuccess(List<String> list) { 
    if (list.isEmpty()) { // 2.收藏夹为空获取建议
      suggestionService.getSuggestions(new Callback<List<Favorite>>() {
        public void onSuccess(List<Favorite> list) { 
          UiUtils.submitOnUiThread(() -> { // 3.使用UI线程处理
            list.stream()
                .limit(5)
                .forEach(uiList::show); 
            });
        }

        public void onError(Throwable error) { 
          UiUtils.errorPopup(error);
        }
      });
    } else {
      list.stream() 
          .limit(5) // 4.通过收藏夹ID获取详情
          .forEach(favId -> favoriteService.getDetails(favId, 
            new Callback<Favorite>() {
              public void onSuccess(Favorite details) {// 5.UI线程处理
                UiUtils.submitOnUiThread(() -> uiList.show(details));
              }
              public void onError(Throwable error) {
                UiUtils.errorPopup(error);
              }
            }
          ));
    }
  }

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

使用Reactor

userService.getFavorites(userId) 
           .flatMap(favoriteService::getDetails) 
           .switchIfEmpty(suggestionService.getSuggestions()) 
           .take(5) 
           .publishOn(UiUtils.uiThreadScheduler()) // 切换到ui线程
           .subscribe(uiList::show, UiUtils::errorPopup); 

如果获取收藏夹超时(800ms)则从缓存中获取,这在回调中实现起来是一个复杂的任务,但是在Reactor将会非常简单。

userService.getFavorites(userId)
           .timeout(Duration.ofMillis(800)) // 增加超时处理
           .onErrorResume(cacheService.cachedFavoritesFor(userId)) //从缓存中获取
           .flatMap(favoriteService::getDetails) 
           .switchIfEmpty(suggestionService.getSuggestions())
           .take(5)
           .publishOn(UiUtils.uiThreadScheduler())
           .subscribe(uiList::show, UiUtils::errorPopup);

CompletableFuture组合操作的例子

我们首先获取ID的列表,然后我们要通过ID获取一个名称(name)和一个统计信息(Stat),并将它们成对组合,所有这些都是异步的。

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");

可以看出CompletableFuture在组合上没有成熟的API,事件编排能也很有限,写起来代码依旧很复杂。

使用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"
);

可以看出Reactor编程可以通过异步化更好的利用当前CPU的资源,更强大的事件编排能力。

响应式流规范

必须具备的能力

响应式流规范规定了响应式流必须具备的能力

  • 可以处理无限的元素。
  • 有序的处理。
  • 在组件之间异步地传递元素。
  • 必须实现非阻塞的背压(backpressure)。

必须实现的API组件

  • Publisher
    public interface Publisher<T> {
      public void subscribe(Subscriber<? super T> s);
    }
    
  • Subscriber
    public interface Subscriber<T> {
      public void onSubscribe(Subscription s);
      public void onNext(T t);
      public void onError(Throwable t);
      public void onComplete();
    }
    
  • Subscription
    public interface Subscription {
      public void request(long n);
      public void cancel();
    }
    
  • Processer
    public interface Processor<T, R> extends Subscriber<T>, Publisher<R> {
    }
    

核心协议

Publisher是一个潜在的无限的(unbounded)有序的(sequenced)元素提供者。Publisher根据从Subscriber接收到的需求发布元素。

订阅后的回调用表达式表示是:

onSubscribe onNext* (onError | onComplete)?

以一个onSubscribe开始,中间有0个或多个onNext,最后有0个或1个onError或onComplete事件。