介绍
主要说明Spring 5.0
响应式编程底层的线程模型
,并且对比Spring WebFlux
的运行容器:Reactor Netty
、Tomcat
响应式编程的动机
典型的Web应用程序
包含几个复杂的交互部分 ,并且他们之间大部分会相互阻塞:比如数据库调用阻塞I/O线程
,但是,其他请求是独立的,可以同时执行,也可以并行执行。
比如,多个用户请求可以被多个线程同时处理,在如今的多核处理器上有着明显的优势,这种并发模型也被称为 thread-per-request model (一个请求对应一个线程):
如上图所示,每个线程一次只能处理一个请求。
虽然基于线程的并发模型为我们解决了部分问题,但它并不能解决我们在单个线程内进行的大多数交互仍在阻塞的事实。此外,就上下文切换而言,线程过多导致在Java
中实现并发的本机线程的成本很高。
随着Web应用程序
面临越来越多的请求,thread-per-request model 模型并不能满足这种并发量。
因此,需要一个种新的并发模型,该模型可以以相对较少的线程数来处理越来越多的请求。这是采用响应式编程的主要动机之一。
响应式编程的并发模型
响应式编程的核心是时间驱动,基于数据流的改变来做出相应的处理。因此,在完全无阻塞的环境中,这可以以更高的资源利用率(CPU
)实现更高的并发。
响应式编程在使用线程实现并发的方式与传统方式有着很大的区别,响应式编程带来的本质区别是异步。
换句话说,程序处理请求的方式从一系列同步操作
转为异步事件流
。
比如:在响应式编程里,调用数据库操作不会阻塞调用方线程,而是返回一个可以被其他线程订阅的Publisher
对象,Subscriber
可以在数据操作完成后得到通知(回掉)。
最重要的是,响应式编程并不强调生成和使用哪些线程事件。重点是将程序构造为异步事件流
。
Publisher
和Subscriber
并不一定需要在同一个线程中,这有助于更好地利用可用线程,从而提高整体并发性。
EventLoop线程
EventLoop
事件循环是实现以更少的线程实现更高的并发性的关键:
上图是事件循环的抽象设计,响应式编程的核心思想:
EventLoop
以单线程持续运行EventLoop
按序从事件队列取出事件进行处理并且在注册相关回掉函数之后立即返回- 阻塞的操作比如:数据库调用、外部服务调用,会触发相关操作完成的回掉
EventLoop
可以触发操作完成通知的回调并将结果发送回原始调用方
EventLoop
模型已经有许多的实现,比如: Node.js, Netty 和 Ngnix,比起传统的web server
( Apache HTTP Server, Tomcat, JBoss)拥有更好的可扩展性
Spring WebFlux 响应式编程
Spring WebFlux
是Spring
在5.0之后推出的响应式web
技术栈:
可以看到的是:Spring WebFlux
和传统的web
框架是平行的关系,并不是来替代Spring MVC
特性:
Spring WebFlux
通过函数式路由扩展了传统的注解(@Controller
)开发模型Spring WebFlux
为Reactive Streams API
适配了不同的HTTP
运行时环境- 因此,能够支持许多响应式运行时环境包括:
Servlet 3.1+
容器Tomcat
,Undertow
、Reactor Netty
- 同时还新增了WebClient,一个响应式、非阻塞、链式API的Http客户端
不同运行时环境的线程模型
响应式应用往往只使用少数几个线程,并充分利用它们。但是,线程的数量和性质取决于选择的实际Reactive Stream API
运行时环境。
Spring WebFlux
已经适配了通过一个名为HttpHandler
的接口适配了不同的运行时环境。这个类只有一个方法,对不同的服务器API(例如Reactor Netty
,Servlet 3.1 API
或Undertow API
)进行抽象。
虽然Reactor Netty
是WebFlux
应用程序中的默认服务器,但是仅声明一个正确的依赖关系即可切换到任何其他受支持的服务器:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-reactor-netty</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
尽管可以通过多种方式观察在JVM
中创建的线程,也可以通过如下方式简单获取:
public void printThreads(){
Thread.getAllStackTraces()
.keySet()
.stream()
.collect(Collectors.toList());
}
Reactor Netty
Reactor Netty
是WebFlux
的默认运行时Server
,不添加其他依赖,启动一个Spring WebFlux
应用查看默认创建的线程信息:
除去JVM的相关线程,在一台四核的机器上能看到以上线程信息,Netty
产生了一堆用于请求处理的工作线程,这些通常不超过可用的CPU
核心数。
Netty
使用Event Driving
(事件循环) 模型为响应式异步提供高度可扩展的并发能力。
EventLoopGroup
管理一个或多个连续运行的EventLoop
。因此,不建议创建比可用CPU
核心数量更多的EventLoop
。
EventLoopGroup
还为每个新创建的Channel
分配一个EventLoop
。因此,在Channel
的生存期内,所有操作均由同一线程执行。
Apache Tomcat
Spring WebFlux
还支持传统的Servlet
容器比如 Apache Tomcat
WebFlux
依赖 Servlet 3.1 +的非阻塞I/O API,使用Servlet
容器时需要通过底层适配,所以不能直接使用Servlet API
使用Tomcat
作为运行时环境线程信息:
可以看到,与Netty
有着很大区别:Tomcat会启动更多的工作线程,默认时是10
Tomcat 5
及更高版本在其Connector
组件中支持NIO
,该组件主要负责接收请求。
Tomcat
核心是Connector
组件实现以支持NIO的线程模型。作为NioEndpoint
模块的一部分,它由Acceptor
,Poller和Worker
组成:
Tomcat
产生一个或多个用于Acceptor
,Poller
和Worker的线程,通常有一个专用于Worker
的线程池。
WebClient 线程模型
WebClient
作为响应式Http Client
是Spring WebFlux
的一部分,可以在需要基于Restful
的通信时使用它,能够创建端到端的全链路响应式应用
使用WebClient
使用WebClient
非常简单,作为Spring WebFlux
一部分,不需要倒入额外依赖:
创建一个简单的Rest
API :
@GetMapping("/index")
public Mono<String> getIndex() {
return Mono.just("Hello World!");
}
使用WebClient
来进行响应式调用:
WebClient.create("http://localhost:8080/index").get()
.retrieve()
.bodyToMono(String.class)
.doOnNext(s -> printThreads());
WebClient线程模型
WebClient
也是使用event loop model
来实现并发,并且依赖底层的运行时环境。
如果使用Reactor Netty,WebClient
将和Netty Server
共用EventLoop
,所以当使用WebClient
时的线程情况和Reactor Netty
中的一样,并不会创建额外的线程。
WebClient
也支持Servlet 3.1+
的运行时环境比如:Jetty
,此时应用的线程情况就非常不同了。
当使用Jetty
时,WebClient
必须要创建自己的EventLoop
:
在某些情况下,为客户端和服务器使用单独的线程池可以提供更好的性能。WebFlux
默认情况下共用EventLoop
,如果需要获得更好的性能可以单独为WebClient
创建线程池。