在目前的jdk 已经迭代22 虚拟线程(携程)来说 ,上下文的切换的压力得到优化,Spring webflux的热度也慢慢不温不火,但是其中的设计思想还是值得了解的
。
Spring WebFlux 是 Spring Framework 5 引入的非阻塞、响应式编程框架,它是基于异步 I/O 模型构建的。WebFlux 提供了一个事件驱动的响应式编程模型,可以处理大量并发请求而不会被 I/O 操作阻塞。WebFlux 可以运行在支持 Reactive Streams 的服务器(如 Netty)上,也可以在支持 Servlet 3.1+ 规范的容器(如 Tomcat、Jetty)上运行。
阻塞 vs. 非阻塞:
同步 vs. 异步:
这里可能一直对bio,nio,aio等问题困扰,这里就要说一下,底层使用到的是netty这种bio框架,所以需要先了解
即使在响应式编程和异步 I/O的模型中,本质上确实涉及多线程的概念,但它与传统的基于阻塞 I/O (BIO) 的多线程模型有显著区别。响应式编程和异步 I/O 通过一种更高效的方式管理线程和资源,从而避免了传统多线程模型的高开销和低效率。
在基于阻塞 I/O 的传统服务器模型(如 Spring MVC),每个请求会分配一个独立的线程。这个线程在等待 I/O 操作(例如数据库查询或外部 API 调用)时会进入阻塞状态,直到操作完成。在这种模型下,如果有多个请求同时到来,服务器需要创建或分配相应数量的线程。这种方式虽然简单直观,但存在一些问题:
在 异步 I/O(如 Spring WebFlux 的实现)中,尽管仍然使用多线程,但其运作方式大大优化,避免了传统阻塞模型中的资源浪费和高开销。异步 I/O 主要通过 事件驱动 和 回调机制 来处理请求,从而达到高并发下的高效性能。
事件循环线程模型:在异步 I/O 模型中,只有少量的线程(通常是 事件循环线程池)用于处理所有 I/O 操作。当请求到达时,系统不会为每个请求分配一个线程,而是将请求放入事件循环中,由少数线程来监控和处理。线程只负责启动 I/O 操作,并在 I/O 操作完成时,通过回调或事件触发进一步的处理。
非阻塞式 I/O:当系统发起一个 I/O 请求(如数据库查询或网络请求)时,线程不会等待该请求完成,而是将操作挂起,并继续处理其他请求或任务。当 I/O 操作完成时,操作系统会通知应用程序(通过回调或事件),并在事件循环中继续处理这个任务的剩余部分。
背压和资源控制:在响应式编程中,通常有背压(Backpressure)机制,它允许消费者(请求处理代码)根据自身的处理能力,来调节数据流的速率,防止服务器资源过载。
特性 | BIO(传统多线程模型) | 异步 I/O(响应式模型) |
---|---|---|
线程分配 | 每个请求一个线程 | 少量线程处理大量请求 |
I/O 操作 | 阻塞等待 | 非阻塞,异步处理 |
资源消耗 | 高,线程在等待 I/O 时浪费资源 | 低,线程被释放,继续处理其他任务 |
并发处理 | 线程数量受限,容易导致资源瓶颈 | 高效利用少量线程,适合高并发场景 |
线程上下文切换 | 高频上下文切换,开销大 | 减少上下文切换,开销小 |
响应性 | 处理时间依赖于线程是否可用和 I/O 阻塞 | 响应更快,避免线程被长时间占用 |
尽管异步 I/O 和响应式编程模型更高效地使用了线程,但多线程仍然是不可避免的。只是:
线程数少:响应式系统通常使用更少的线程,比如基于 事件循环 的 Reactor Netty 或 Vert.x 通常只有固定数量的 I/O 线程,不会为每个请求创建新的线程。
线程不会被阻塞:线程不再是处理 I/O 操作的核心部分,而是负责调度和管理事件流。一旦发起 I/O 操作,线程会被释放出来,继续处理其他任务。
线程池管理:响应式系统通常会有不同的线程池来处理不同类型的任务。比如,I/O 操作可能由一个线程池处理,而 CPU 密集型任务由另一个线程池处理。这种分离使得系统在处理不同任务时更高效。
异步和回调:响应式编程强调 异步,操作通常通过回调链或响应式流(如 Mono
和 Flux
)来完成,而不是通过线程阻塞来等待结果。这样可以充分利用服务器资源,即使在高并发情况下也能保持高效。
因此,异步 I/O 和响应式编程中的“多线程”不同于传统的多线程模型,它依赖于少量线程的高效利用和异步非阻塞的设计理念,能够在处理大量并发请求时保持高效的性能。
要理解 Spring WebFlux 的核心异步和非阻塞编程模型,首先需要理解一些底层概念。
在传统的阻塞 I/O 模型中,一个线程发起一个 I/O 操作(如读取文件或数据库查询),它会被阻塞,即必须等待 I/O 操作完成,才能继续执行后续的代码逻辑。这个线程在等待期间无法处理其他任务。
缺点:
在非阻塞 I/O 模型中,线程发起 I/O 操作后不会被阻塞,而是立即被释放。I/O 操作在后台继续进行,线程可以继续执行其他任务。当 I/O 操作完成时,系统会通过回调机制或事件通知来告知线程操作已完成,线程再继续处理操作结果。
非阻塞 I/O 具体步骤:
异步 I/O 通常与非阻塞 I/O 配合使用。异步 I/O 模型的特点是,I/O 操作不会在发起时阻塞线程,而是通过回调或事件机制来异步处理任务。
异步 I/O 和非阻塞 I/O 的区别:
所以其实说简单一点就是,
传统的mvc服务器会每次收到一个请求就新建一个线程,来处理这个请求,而Spring webflux这种异步服务器类似线程池,web服务器会提前维护一批线程,用多路复用的方式,每次到达的请求就采用之前维护的线程进行处理请求,这样就不会每次一个线程就处理资源,
WebFlux 和其他基于异步 I/O 的服务器模型确实与 线程池 有很大相似之处,它们通过维护一组固定数量的线程(如 事件循环线程池)来处理大量的请求,而不为每个请求单独创建新线程。
事件循环和线程池:
epoll
或 select
,它们可以监听多个 I/O 操作,一旦有数据可用或操作完成,系统就会通知事件循环线程去处理。多路复用的工作原理:
虽然线程池和事件循环的线程是固定数量的,但性能不会受到太大影响,原因在于以下几个关键点:
非阻塞 I/O:
少量线程处理大量任务:
背压(Backpressure)机制:
减少线程上下文切换:
高效的线程利用率:
因为线程不被 I/O 操作阻塞,所以即使线程数量有限,每个线程的利用率会非常高。当一个线程等待 I/O 操作完成时,它可以去处理其他请求的业务逻辑。这样可以提高线程的并发处理能力,减少系统在高并发情况下的响应延迟。
异步回调机制:
通过异步编程模型,I/O 操作的完成不再依赖于线程阻塞,而是通过回调机制通知相关线程继续处理剩余任务。这种模式下,系统可以快速处理请求的部分业务逻辑,并通过异步方式等待 I/O 操作完成。最终的响应时间是由整体的异步任务完成时间决定的,而不是由单个线程的阻塞状态决定。
事件驱动响应更快:
非阻塞 I/O 模型通过事件驱动方式处理请求,因此在高并发的情况下,服务器能迅速对请求做出反应,而不会因为线程资源耗尽导致响应时间增加。通过优化的 I/O 多路复用机制,系统可以快速侦听和处理大量请求,而不会因为线程数量有限而延迟响应。
WebFlux 的确通过 线程池 和 多路复用 的方式来处理请求,避免了传统每个请求都创建新线程的做法。虽然维护的线程数量是固定的,但由于采用了 非阻塞 和 异步事件驱动 的机制,线程并不会因为等待 I/O 而被阻塞,从而能够同时处理多个请求。这样一来,即使线程数量有限,系统仍然能够高效处理高并发请求,保持良好的响应时间。
在理解 WebFlux 时,最核心的点是事件驱动模型和I/O 多路复用,以及它如何通过少量线程处理大量并发请求。
尽管 WebFlux 使用的是非阻塞 I/O,但它仍然依赖一个小型线程池来处理业务逻辑。每个线程不需要等待 I/O 操作的完成,可以并发地处理多个请求,从而减少线程上下文切换的开销。
在 WebFlux 中,处理请求的结果通常以两种响应类型表示:Mono
和 Flux
。
webflux构建的异步服务器中,返回的响应都是通过flux和mono包裹的异步序列
示例:
/*
包裹像响应结构题体
*/
@GetMapping
public Mono<String> getHelloMessage() {
return Mono.just("Hello, WebFlux!");
}
@GetMapping("2")
public Flux<String> getNames() {
return Flux.just("John", "Jane", "Jack");
}
数据订阅
result.subscribe(s -> log.info("get2 result: {}", s));
just 是立即包裹数据在异步响应中,类似传统mvc的响应体 ,线程会阻塞在这里等待包装结果
如果处理结果比较耗时也可以采用异步包装
// 阻塞5秒钟 模拟任务耗时
private String createStr() {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
}
return "some string";
}
// 普通的SpringMVC方法
@GetMapping("/1")
private String get1() {
log.info("get1 start");
String result = createStr();
log.info("get1 end.");
return result;
}
// WebFlux(返回的是Mono)
@GetMapping("/2")
private Mono<String> get2() {
log.info("get2 start");
Mono<String> result = Mono.fromSupplier(() -> createStr());
log.info("get2 end.");
return result;
}
代码到这里,很多时候,会把nio和多线程搞混了,这里肯定会想,线程执行这个方法,而此时不会阻塞等待结果执行完成,所以会认为当这个方法处理时间过长,返回响应的结果就为null
这样其实是很并发情况理解混乱了
由于nio的特性当代码块执行到这个createStr()时候,并不会阻塞,而是继续执行下面的代码,如果 createStr() 方法耗时很长,它的执行会被延迟到消费者(客户端)订阅 Mono 时。因此在 get2() 方法返回时,result 变量并不会导致返回 null。实际上是返回了一个包装了 createStr() 的 Mono 对象,而不是 createStr() 的结果,
Mono.fromSupplier(() -> createStr()) 会创建一个 Mono 实例,并将 createStr() 方法包装成一个供应者(supplier)。
此时,createStr() 不会被立即执行,只有在订阅 Mono 时,它才会执行。而在webflux构建的服务器中,用户的访问请求就类似一个隐式的订阅,mono包裹这个行为给客户端,客户端获取到这个行为,然后等当有客户端请求来订阅它时,createStr() 方法才会被调用。
如果 createStr() 在调用时耗时过长,客户端会在等待结果,但此时不会返回 null。相反,Spring WebFlux 会保持连接,直到 createStr() 完成,并通过 Mono 返回结果。
并且支持传统的mvc方式注册Controller·路由
@RestController
@RequestMapping("/api")
public class BasicController {
@GetMapping("/names")
public Flux<String> queryNmaes() {
return Flux.just("Hello", "World", "张三", "李四");
}
@GetMapping("/users")
public Mono<R<User>> queryUser() {
User user = new User();
user.setName("测试对象");
user.setAge(666);
return Mono.just(R.<User>builder().t(user)
.msg("success")
.build());
}
}
也可以向gin和flask等web框架链式注册路由
@Bean
public RouterFunction<ServerResponse> routes(WebHandler handler) {
return route()
.GET("/hello", handler::hello)
.build();
}
还有一个和mvc的不同就是流式处理,相当于mvc同步模型的stream流采用, Spring WebFlux 通过 Reactor 框架实现响应式编程。常用的操作符有 map
, flatMap
, filter
, subscribe
等。
public Mono<String> getGreeting(String name) {
return Mono.just(name)
//数据格式转换
.map(n -> "Hello, " + n)
//一个数据转换为多个数据
.flatMap(greeting -> Mono.just(greeting + " from WebFlux!"));
}
是的,你的理解是正确的。在 Spring WebFlux 中,Flux
提供了一种响应式编程的方式,类似于 Java 8 中的流式编程,但它是针对异步数据流的处理。
Flux.merge()
是一个非常有用的方法,它可以将多个 Flux
实例合并成一个单一的 Flux
实例。它会将所有传入的 Flux
中的数据流汇聚到一起,并保持它们的异步特性。
下面我们详细解析一下你的示例代码:
Flux.merge(
Flux.just(1, 2, 3).delayElements(Duration.ofMillis(1)),
Flux.just(4, 5, 6).delayElements(Duration.ofMillis(1))
).subscribe(System.out::println);
Flux.just(1, 2, 3)
和 Flux.just(4, 5, 6)
分别创建了两个 Flux
实例,分别包含了数字 1、2、3 和 4、5、6。
.delayElements(Duration.ofMillis(1))
会让每个元素在发出前延迟 1 毫秒。这个方法的作用是模拟数据流的延迟,这在测试异步行为时特别有用。
Flux.merge(...)
将两个 Flux
实例合并成一个新的 Flux
实例。这个新的 Flux
将会发出来自所有输入 Flux
的元素,而不需要等待它们完成。
最后,.subscribe(System.out::println)
表示对合并后的 Flux
进行订阅。每当有元素发出时,System.out::println
会被调用,从而打印出这些元素。类似foreach
示例输出
由于每个 Flux
都有 1 毫秒的延迟,输出将会是:
1
4
2
5
3
6
输出的顺序是交替的,因为 Flux.merge()
会将来自两个源的元素混合在一起。
除了 merge
方法,Flux
还提供了许多其他有用的 API,以下是一些常用的方法:
map: 对每个元素应用一个函数,并返回一个新的 Flux
。
//把流的原素依次变大俩倍
Flux<Integer> map = Flux.just(1, 2, 3)
.map(i -> i * 2);
map.subscribe(System.out::println);
filter: 只保留满足条件的元素。
Flux.just(1, 2, 3, 4, 5)
.filter(i -> i % 2 == 0) // 输出: 2, 4
.subscribe(System.out::println);
flatMap: 将元素转换为多个元素的流,并将结果扁平化为一个 Flux
。
Flux.just(1, 2, 3)
.flatMap(i -> Flux.just(i, i * 10)) // 输出: 1, 10, 2, 20, 3, 30
.subscribe(System.out::println);
reduce: 对所有元素应用一个归约操作,最终返回一个单一结果。
Flux.just(1, 2, 3)
.reduce(0, Integer::sum) // 输出: 6
.subscribe(System.out::println);
collect 收集流中的原素
Flux<Integer> numbersFlux = Flux.just(1, 2, 3);
Mono<List<Integer>> listMono = numbersFlux.collectList(); // 收集成 List
Flux
是异步的,可以在等待 I/O 操作的同时处理其他任务。它通过非阻塞的方式来提高系统的响应能力和处理能力。Spring WebFlux 的 API 和 Java 8 的流式 API 在语法上看起来相似,但在处理数据的方式上有很大的不同
在非阻塞 I/O 模型中,当一个线程发出 I/O 请求后,它并不会阻塞等待结果,而是立即释放,去处理其他任务。而对于这个 I/O 请求的处理,通常有以下的机制来完成:
当线程发起 I/O 请求时,具体的 I/O 操作(如网络通信、文件读取等)不会在应用层的线程中完成,而是由操作系统内核或I/O 子系统来负责。操作系统会负责执行实际的 I/O 操作,比如等待数据从网络中传输过来,或者等待磁盘完成读取。
epoll
、select
、kqueue
等)来监听和管理 I/O 事件。当某个 I/O 操作完成时,操作系统会发出通知,告知应用层的线程可以继续处理接下来的步骤。当 I/O 操作完成时,操作系统会通知应用层,应用层通常通过回调机制处理后续的逻辑。回调函数可以在一个事件循环中被执行,或者在线程池中重新分配给某个线程来执行。此时,真正执行这部分代码的是一个事件驱动线程或一个新分配的线程,而不是原来发出 I/O 请求的线程。
事件循环线程:如果是像 Netty 这样的框架,它内部会维护一个事件循环(event loop),该循环由少量线程组成,负责监听 I/O 操作的完成事件。当操作系统通知某个 I/O 操作完成后,事件循环会选择一个空闲的线程去执行该 I/O 操作的后续处理代码(即回调函数)。
线程池中的线程:回调逻辑有时也可以交给一个线程池中的线程来处理。这种情况下,线程池中的某个空闲线程会执行回调函数,并继续完成 I/O 操作后的逻辑。
举个例子来说明:
在 WebFlux 中,少量线程可以处理大量并发请求。通过异步 I/O 和事件驱动机制,线程池中的线程不会因为等待 I/O 操作而被阻塞,可以并行处理其他任务。
传统的多线程阻塞 I/O 模型中,线程的上下文切换是性能瓶颈之一。WebFlux 通过少量线程高效处理 I/O 请求,减少了线程之间的切换和阻塞,从而提高了整体系统性能。
在传统的 Spring MVC 中,ThreadLocal
通常用于存储每个请求的用户上下文信息,因为每个请求都在独立的线程中处理,这样可以确保线程间不会相互干扰。然而,在 Spring WebFlux 中,由于采用了响应式编程模型和非阻塞 I/O,ThreadLocal
的使用并不适合,因为 WebFlux 使用的是反应式流,并且一个线程可能会处理多个请求。
在 WebFlux 中,由于不能使用threadlocal,所以使用 reactor.util.context.Context
来管理请求的上下文。这个上下文可以在异步执行的链中传递,确保每个请求在不同的上下文中运行,而不会互相影响。
Context
存储用户上下文信息创建过滤器:首先,你可以创建一个 WebFilter
,在其中提取用户上下文信息并将其存储到 Context
中。
使用 deferContextual
读取上下文:在处理请求的过程中,可以通过 Mono.deferContextual
方法读取当前的上下文。
以下是一个如何在 WebFlux 中使用 Context
存储和获取用户上下文信息的示例:
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
@Component
public class UserContextFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
// 从请求头中获取用户ID
String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
// 将用户ID存储到上下文中
return chain.filter(exchange)
.contextWrite(Context.of("userId", userId)); // 将用户 ID 写入上下文
}
}
在服务或控制器中,你可以使用 deferContextual
来访问上下文中的用户信息:
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
@Service
public class UserService {
public Mono<String> getUserData() {
return Mono.deferContextual(context -> {
// 从上下文中获取用户ID
String userId = context.get("userId");
// 根据用户 ID 进行处理
return Mono.just("Data for user: " + userId);
});
}
}
在 Spring WebFlux 中,使用
Context
来存储和访问用户上下文信息,而不是使用ThreadLocal
。这种设计允许在异步和非阻塞的环境中有效地管理上下文,确保每个请求都在其独立的上下文中处理。这种方法避免了线程间的干扰,更加适合响应式编程模型。
Spring Security 提供了对 WebFlux 的支持,它也是非阻塞的,可以无缝集成到 WebFlux 中。
在 WebFlux 中,Spring Security 依赖 SecurityWebFilterChain
来处理请求。这个过滤器链是基于响应式流的,能够与异步 I/O 模型一起工作。
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.authorizeExchange()
.pathMatchers("/admin/**").hasRole("ADMIN")
.anyExchange().authenticated()
.and().build();
}
在 WebFlux 中,传统的 ThreadLocal
上下文管理方式不再适用,因为请求可能不会在同一个线程上完成。为了解决这个问题,WebFlux 提供了 Context
对象,可以在整个请求生命周期中传递安全上下文。
SecurityContext context = ReactiveSecurityContextHolder.getContext().block();
阻塞时间**:传统的线程池模型容易受限于线程上下文切换和阻塞 I/O 操作,WebFlux 通过减少这些瓶颈提高了系统的响应速度。