现在, Java 的各种基于 Reactor 模型的响应式编程库或者框架越来越多了,像是 RxJava,Project Reactor,Vert.x 等等等等。在 Java 9, Java 也引入了自己的 响应式编程的一种标准接口,即java.util.concurrent.Flow这个类。这个类里面规定了 Java 响应式编程所要实现的接口与抽象。我们这个系列要讨论的就是Project Reactor这个实现。

这里也提一下,为了能对于没有升级到 Java 9 的用户也能兼容,java.util.concurrent.Flow这个类也被放入了一个 jar 供 Java 9 之前的版本,依赖是:

<dependency><groupId>org.reactivestreams</groupId><artifactId>reactive-streams</artifactId><version>1.0.3</version>
</dependency>

本系列所讲述的 Project Reactor 就是 reactive-streams 的一种实现。 首先,我们先来了解下,什么是响应式编程,Java 如何实现

什么是响应式编程,Java 如何实现

我们这里用通过唯一 id 获取知乎的某个回答作为例子,首先我们先明确下,一次HTTP请求到服务器上处理完之后,将响应写回这次请求的连接,就是完成这次请求了,如下:

public void request(Connection connection, HttpRequest request) {//处理request,省略代码connection.write(response);//完成响应
}

假设获取回答需要调用两个接口,获取评论数量还有获取回答信息,传统的代码可能会这么去写:

//获取评论数量
public void getCommentCount(Connection connection, HttpRequest request) {Integer commentCount = null;try {//从缓存获取评论数量,阻塞IOcommentCount = getCommnetCountFromCache(id);} catch(Exception e) {try {//缓存获取失败就从数据库中获取,阻塞IOcommentCount = getVoteCountFromDB(id);} catch(Exception ex) {}}connection.write(commentCount);
}//获取回答
public void getAnswer(Connection connection, HttpRequest request) {//获取点赞数量Integer voteCount = null;try {//从缓存获取点赞数量,阻塞IOvoteCount = getVoteCountFromCache(id);} catch(Exception e) {try {//缓存获取失败就从数据库中获取,阻塞IOvoteCount = getVoteCountFromDB(id);} catch(Exception ex) {}}//从数据库获取回答信息,阻塞IOAnswer answer = getAnswerFromDB(id);//拼装ResponseResultVO response = new ResultVO();if (voteCount != null) {response.setVoteCount(voteCount);}if (answer != null) {response.setAnswer(answer);}connection.write(response);//完成响应
}

在这种实现下,你的进程只需要一个线程池,承载了所有请求。这种实现下,有两个弊端:

  1. 线程池 IO 阻塞,导致某个存储变慢或者缓存击穿的话,所有服务都堵住了。假设现在评论缓存突然挂了,全都访问数据库,导致请求变慢。由于线程需要等待 IO 响应,导致唯一一个线程池被堆满,无法处理获取回答的请求。
  2. 对于获取回答信息,获取点赞数量其实和获取回答信息是可以并发进行的。不用非得先获取点赞数量之后再获取回答信息。

现在,NIO 非阻塞 IO 很普及了,有了非阻塞 IO,我们可以通过响应式编程,来让我们的线程不会阻塞,而是一直在处理请求。这是如何实现的呢?

传统的 BIO,是线程将数据写入 Connection 之后,当前线程进入 Block 状态,直到响应返回,之后接着做响应返回后的动作。NIO 则是线程将数据写入 Connection 之后,将响应返回后需要做的事情以及参数缓存到一个地方之后,直接返回。在有响应返回后,NIO 的 Selector 的 Read 事件会是 Ready 状态,扫描 Selector 事件的线程,会告诉你的线程池数据好了,然后线程池中的某个线程,拿出刚刚缓存的要做的事情还有参数,继续处理。

那么,怎样实现缓存响应返回后需要做的事情以及参数的呢?Java 本身提供了两种接口,一个是基于回调的 Callback 接口(Java 8 引入的各种Functional Interface),一种是 Future 框架。

基于 Callback 的实现:

//获取回答
public void getAnswer(Connection connection, HttpRequest request) {ResultVO resultVO = new ResultVO();getVoteCountFromCache(id, (count, throwable) -> {//异常不为null则为获取失败if (throwable != null) {//读取缓存失败就从数据库获取getVoteCountFromDB(id, (count2, throwable2) -> {if (throwable2 == null) {resultVO.setVoteCount(voteCount);}//从数据库读取回答信息getAnswerFromDB(id, (answer, throwable3) -> {if (throwable3 == null) {resultVO.setAnswer(answer);connection.write(resultVO);} else {connection.write(throwable3);}});});} else {//获取成功,设置voteCountresultVO.setVoteCount(voteCount);//从数据库读取回答信息getAnswerFromDB(id, (answer, throwable2) -> {if (throwable2 == null) {resultVO.setAnswer(answer);//返回响应connection.write(resultVO);} else {//返回错误响应connection.write(throwable2);}});}});
}

可以看出,随着调用层级的加深,callback 层级越来越深,越来越难写,而且啰嗦的代码很多。并且,基于 CallBack 想实现获取点赞数量其实和获取回答信息并发是很难写的,这里还是先获取点赞数量之后再获取回答信息。

那么基于 Future 呢?我们用 Java 8 之后引入的 CompletableFuture 来试着实现下。

//获取回答
public void getAnswer(Connection connection, HttpRequest request) {ResultVO resultVO = new ResultVO();//所有的异步任务都执行完之后要做的事情CompletableFuture.allOf(getVoteCountFromCache(id)//发生异常,从数据库读取.exceptionallyComposeAsync(throwable -> getVoteCountFromDB(id))//读取完之后,设置VoteCount.thenAccept(voteCount -> {resultVO.setVoteCount(voteCount);}),getAnswerFromDB(id).thenAccept(answer -> {resultVO.setAnswer(answer);})).exceptionallyAsync(throwable -> {connection.write(throwable);}).thenRun(() -> {connection.write(resultVO);});
}

这种实现就看上去简单多了,并且读取点赞数量还有读取回答内容是同时进行的。 Project Reactor 在 Completableuture 这种实现的基础上,增加了更多的组合方式以及更完善的异常处理机制,以及面对背压时候的处理机制,还有重试机制

响应式编程里面遇到的问题 - 背压

由于响应式编程,不阻塞,所以把之前因为基本不会发生而忽视的一个问题带了上来,就是背压(Back Pressure)。

背压是指,当上游请求过多,下游服务来不及响应,导致 Buffer 溢出的这样一个问题。在响应式编程,由于线程不阻塞,遇到 IO 就会把当前参数和要做的事情缓存起来,这样无疑增大了很多吞吐量,同时内存占用也大了起来,如果不限制的话,很可能 OutOfMemory,这就是背压问题。

在这个问题上,Project Reactor 基于的模型,是有处理方式的,Completableuture 这个体系里面没有。

为何现在响应式编程在业务开发微服务开发不普及

主要因为数据库 IO,不是 NIO。

不论是Java自带的Future框架,还是 Spring WebFlux,还是 Vert.x,他们都是一种非阻塞的基于Ractor模型的框架(后两个框架都是利用netty实现)。

在阻塞编程模式里,任何一个请求,都需要一个线程去处理,如果io阻塞了,那么这个线程也会阻塞在那。但是在非阻塞编程里面,基于响应式的编程,线程不会被阻塞,还可以处理其他请求。举一个简单例子:假设只有一个线程池,请求来的时候,线程池处理,需要读取数据库 IO,这个 IO 是 NIO 非阻塞 IO,那么就将请求数据写入数据库连接,直接返回。之后数据库返回数据,这个链接的 Selector 会有 Read 事件准备就绪,这时候,再通过这个线程池去读取数据处理(相当于回调),这时候用的线程和之前不一定是同一个线程。这样的话,线程就不用等待数据库返回,而是直接处理其他请求。这样情况下,即使某个业务 SQL 的执行时间长,也不会影响其他业务的执行。

但是,这一切的基础,是 IO 必须是非阻塞 IO,也就是 NIO(或者 AIO)。官方JDBC没有 NIO,只有 BIO 实现。这样无法让线程将请求写入链接之后直接返回,必须等待响应。但是也就解决方案,就是通过其他线程池,专门处理数据库请求并等待返回进行回调,也就是业务线程池 A 将数据库 BIO 请求交给线程池B处理,读取完数据之后,再交给 A 执行剩下的业务逻辑。这样A也不用阻塞,可以处理其他请求。但是,这样还是有因为某个业务 SQL 的执行时间长,导致B所有线程被阻塞住队列也满了从而A的请求也被阻塞的情况,这是不完美的实现。真正完美的,需要 JDBC 实现 NIO。

Java 自带的 Future框架可以这么用JDBC:

@GetMapping
public DeferredResult<Result> get() {
DeferredResult<Result> deferredResult = new DeferredResult<>();
CompletableFuture.supplyAsync(() -> {return 阻塞数据库IO;//dbThreadPool用来处理阻塞的数据库IO}, dbThreadPool).thenComposeAsync(result -> {//spring 的 DeferredResult 来实现异步回调写入结果返回deferredResult.setResult(result);
});
return deferredResult;
}

WebFlux 也可以使用阻塞JDBC,但是同理:

@GetMapping
public Mono<Result> get() {
return Mono.fromFuture(CompletableFuture.supplyAsync(() -> {return 阻塞数据库IO;//dbThreadPool用来处理阻塞的数据库IO}, dbThreadPool));
}

Vert.x 也可以使用阻塞的JDBC,也是同理:

@GetMapping
public  DeferredResult<Result> get() {
DeferredResult<Result> deferredResult = new DeferredResult<>();
getResultFromDB().setHandler(asyncResult -> {if (asyncResult.succeeded()) {deferredResult.setResult(asyncResult.result());} else {deferredResult.setErrorResult(asyncResult.cause());}});
return deferredResult;
}private WorkerExecutor dbThreadPool = vertx.createSharedWorkerExecutor("DB", 16);private Future<Result> getResultFromDB() {Future<Result> result = Future.future();dbThreadPool.executeBlocking(future -> {return 阻塞数据库IO;}, false, asyncResult -> {if (asyncResult.succeeded()) {result.complete(asyncResult.result());} else {result.fail(asyncResult.cause());}});return result;
}

相当于通过另外的线程池(当然也可以通过原有线程池,反正就是要用和请求不一样的线程,才能实现回调,而不是当次就阻塞等待),封装了阻塞 JDBC IO。

但是,这样几乎对数据库IO主导的应用性能没有提升,还增加了线程切换,得不偿失。所以,需要使用真正实现了 NIO 的数据库客户端。目前有这些 NIO 的 JDBC 客户端,但是都不普及:

  1. Vert.x 客户端:https://vertx.io/docs/vertx-jdbc-client/java/
  2. r2jdbc 客户端:http://r2dbc.io/
  3. Jasync-sql 客户端:https://github.com/jasync-sql/jasync-sql

response获取响应内容_Project Reactor 深度解析 - 1. 响应式编程介绍,实现以及现有问题相关推荐

  1. Reactor (1)Mono和Flux进行响应式编程介绍

    Reactor Mono和Flux进行反应式编程 官网:https://projectreactor.io/ 教程:https://projectreactor.io/docs/core/releas ...

  2. java爬虫获取div内容_Java爬虫-简单解析网页内容

    获取百度新闻中所有的中国新闻的标题时间来源 1 获取网页2 public static String getContent(String str) throwsClientProtocolExcept ...

  3. Aspects深度解析-iOS面向切面编程

    ????????关注后回复 "进群" ,拉你进程序员交流群???????? 作者丨monkery 来源丨码上work(codework88) 背景简述 在日常开发过程中是否有过这样 ...

  4. python深度讲解_《深度剖析CPython解释器》21. Python类机制的深度解析(第五部分): 全方位介绍Python中的魔法方法,一网打尽...

    楔子 下面我们来看一下Python中的魔法方法,我们知道Python将操作符都抽象成了一个魔法方法(magic method),实例对象进行操作时,实际上会调用魔法方法.也正因为如此,numpy才得以 ...

  5. SEO技术深度解析(TF-IDF算法原理及公式)

    做为SEO行业老鸟应该听说过TF-IDF算法,TF-IDF算法属于搜索引擎中的核心部分.TF-IDF算法是增加相关词的覆盖率,以及高优布局关键词密度,从而在百度谷歌等搜索引擎内容质量这一项上的排名加分 ...

  6. Reactor响应式编程

    Reactor响应式编程 介绍响应式编程 响应式编程(reactive programming)是一种基于数据流(data stream)和变化传递(propagation of change)的声明 ...

  7. Reactor响应式编程 之 简介

    1 reactor 出现的背景.初衷和要达到什么样的目标 Reactor 项目始于 2012 年. 经过长时间的内部孵化,于 2013 年发布 Reactor 1.x 版本. Reactor 1 在各 ...

  8. 使用Reactor进行反应式编程最全教程

    反应式编程(Reactive Programming)这种新的编程范式越来越受到开发人员的欢迎.在 Java 社区中比较流行的是 RxJava 和 RxJava 2.本文要介绍的是另外一个新的反应式编 ...

  9. Reactive(1) 从响应式编程到好莱坞

    目录 概念 面向流设计 异步化 响应式宣言 参考文档 概念 Reactive Programming(响应式编程)已经不是一个新东西了. 关于 Reactive 其实是一个泛化的概念,由于很抽象,一些 ...

最新文章

  1. sqlserver compact sdf, sqlite 数据库 在net中相对路径设置方法 - 摘自网络
  2. Hadoop namenode启动瓶颈分析
  3. android paint 圆角 绘制_[BOT] 一种android中实现“圆角矩形”的方法
  4. [ BZOJ 4668 ] 冷战
  5. 浅谈web开发以及django的安装和入门
  6. python3 缺少PIP解决办法
  7. ListView控件 1130
  8. 条款01:视C++为一个语言联邦
  9. [CQOI2014]数三角形 题解(找规律乱搞)
  10. clickhouse代理Chproxy
  11. 解决鼠标右键中没有新建选项
  12. Python每天一个小程序——字典翻转输出和《沉默的羔羊》之最多单词
  13. Menhera酱全套表情包
  14. Gephi启动错误:Cannot load even default layout, using internally predefined
  15. android手机定时截屏软件,最好用的截图软件 安卓手机截图软件横评对比
  16. PCB、SCH转化为AD工程
  17. ubuntu配置mta_如何在Ubuntu 18.04上使用Apache为您的域配置MTA-STS和TLS报告
  18. Thread.currentThread()、isAlive()、Thread.sleep()的使用
  19. 三维空间——点线面关系
  20. 【舆情监测平台】舆情危机处置的四大原则。

热门文章

  1. 哈工大理论力学第八版电子版_理论力学哈工大第八版1第六章思考题课后题
  2. python多继承顺序_Python多继承以及MRO顺序的使用
  3. python模拟火车订票系统_如何用python编写火车抢票助手
  4. php isapi mysql_windows server 2003以isapi的方式配置php+mysql环境的详细过程
  5. 深入浅出之string
  6. Faster RCNN 训练中的一些问题及解决办法
  7. 【嵌入式Linux学习七步曲之第五篇 Linux内核及驱动编程】PowerPC + Linux2.6.25平台下的I2C驱动架构分析
  8. 使用register_chrdev注册字符设备
  9. OpenCV计算图像的平均值和标准差的函数meanStdDev函数的使用
  10. 传神成进博会唯一指定智能翻译硬件提供商 力助无障碍沟通