Spring Webflux - 01 MVC的困境

简介: MVC的困境

@[toc]

在这里插入图片描述


Spring MVC的困境

我们先看一段工作中大家常见的代码

@RestController
public class TestAController {
@RequestMapping(value ="resource",method = RequestMethod.GET)
public Object processResult(){
     RestTemplate restTemplateew RestTemplate();
     ∥请求外部资源
    String result =  restTemplate. getForObject("http://example.com/api/resource2", String.class)
    return processResultFurther(result);
}

private String processResultFurther(String result){
    return "resource here"
}

Tomcat处理请求,线程状态的变化如下:
在这里插入图片描述

我们发现这里的请求和响应事实上 是 同步阻塞

再深入想一下,如果每个线程的执行时间是不可控的,而Tomcat线程池中的线程数量是有限的........

那该怎么办呢?

在这里插入图片描述


Servlet 异步请求缓解线程池压力

我们来算一下:

  • TPS : 2000/s
  • 请求耗时:250ms

那么在这种情况下:

  • tomcat最大线程数配置: 2000/s * 0.25s=500

因此 server. tomcat. threads. max=500 基本能满足需求

那假设 tps 到了 4000 呢?

虽然我们可以扩大线程数量,但线程是要消耗操作系统资源的,也并非越多越好,当然了还有其他很多影响因素。

那怎么办呢?
在这里插入图片描述


Servlet 3.0 异步请求处理

Filter/Servlet在生成响应之前可能要等待一些资源的响应以完成请求处理,比如一个jdbc查询,或者远程服务rpc调用。

Servlet阻塞等待是一个低效的操作,这将导致受限系统资源急剧紧张,比如线程数、连接数等等

Servlet 3.0引入了异步处理请求的能力,使得线程可以不用阻塞等待,提早返回到容器,从而执行更多的任务请求。把耗时的任务提交给另一个异步线程去执行,以及产生响应


Code 演示

工程

在这里插入图片描述


pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.artisan</groupId>
    <artifactId>servlet-asyn</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>


    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.7.4</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
        </dependency>
    </dependencies>

</project>

配置文件

server.tomcat.threads.max=1

启动类

package com.artisan;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;

/**
 * @author 小工匠
 * @version 1.0
 * @description: 启动类
 * @date 2022/10/6 12:03
 * @mark: show me the code , change the world
 */

@SpringBootApplication
// 使用@ServletComponentScan注解后,Servlet、Filter、Listener可以直接通过@WebServlet、@WebFilter、@WebListener注解自动注册,无需其他代码
@ServletComponentScan
public class ServletAsyncApplication {


    public static void main(String[] args) {
        SpringApplication.run(ServletAsyncApplication.class,args);
    }
}
    

同步servlet

package com.artisan.servlet;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

/**
 * @author 小工匠
 * @version 1.0
 * @description: 同步Servlet请求
 * @date 2022/10/6 12:06
 * @mark: show me the code , change the world
 */

@WebServlet(value = "/sync")
@Slf4j
public class SyncServlet extends HttpServlet {


    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        log.info("request: {}, currentThread: {}", req.getQueryString(), Thread.currentThread().getName());

        processFuture(req, resp);
    }

    private void processFuture(HttpServletRequest req, HttpServletResponse resp) {
        try {
            TimeUnit.SECONDS.sleep(10);
            resp.getWriter().println("sync handler");
        } catch (IOException  | InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

}
    

演示

在这里插入图片描述


异步servlet

package com.artisan.servlet;

import com.artisan.handler.AsyncRequestWrapper;
import com.artisan.handler.AsyncServletRejectedHandler;
import com.artisan.handler.AsyncThreadFactory;
import lombok.extern.slf4j.Slf4j;

import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author 小工匠
 * @version 1.0
 * @description: 异步Servlet请求
 * @date 2022/10/6 15:23
 * @mark: show me the code , change the world
 */

@WebServlet(value = "/async", asyncSupported = true)
@Slf4j
public class AsyncServlet extends HttpServlet {


    private ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.SECONDS,
            new LinkedBlockingDeque<>(1),
            AsyncThreadFactory.builder().threadName("async-thread-pool").build(),
            new AsyncServletRejectedHandler());


    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        log.info("request: {}, currentThread: {}", req.getQueryString(), Thread.currentThread().getName());

        AsyncContext asyncContext = req.startAsync();

        AsyncRequestWrapper wrapper = AsyncRequestWrapper.builder().asyncContext(asyncContext)
                .servletRequest(req)
                .servletResponse(resp)
                .thread(Thread.currentThread())
                .build();

        executor.execute(wrapper);
    }
}
    

辅助Code

【AsyncThreadFactory 】

package com.artisan.handler;

import lombok.Builder;

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author 小工匠
 * @version 1.0
 * @description: 线程池工厂
 * @date 2022/10/6 15:35
 * @mark: show me the code , change the world
 */

@Builder
public class AsyncThreadFactory implements ThreadFactory {

    private final ThreadFactory threadFactory = Executors.defaultThreadFactory();

    private String threadName;

    private final AtomicInteger atomicInteger = new AtomicInteger(1);

    @Override
    public Thread newThread(Runnable r) {
        Thread thread = threadFactory.newThread(r);
        thread.setName(this.threadName + "-" + atomicInteger.getAndIncrement());
        return thread;
    }
}
    

【AsyncRequestWrapper 】

package com.artisan.handler;

import lombok.Builder;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import javax.servlet.AsyncContext;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

/**
 * @author 小工匠
 * @version 1.0
 * @description: 线程包装对象
 * @date 2022/10/6 15:42
 * @mark: show me the code , change the world
 */

@Slf4j
@Data
@Builder
public class AsyncRequestWrapper implements Runnable {

    private AsyncContext asyncContext;
    private ServletRequest servletRequest;
    private ServletResponse servletResponse;
    private Thread thread;

    @Override
    public void run() {
        processFuture(asyncContext, servletRequest, servletResponse);
    }

    private void processFuture(AsyncContext asyncContext, ServletRequest servletRequest, ServletResponse servletResponse) {

        HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;

        try {
            TimeUnit.SECONDS.sleep(10);
            log.info("processFuture 当前处理线程 {}",  Thread.currentThread().getName());
            servletResponse.getWriter().println("async handler -->" + httpServletRequest.getQueryString());
        } catch (IOException | InterruptedException e) {
            throw new RuntimeException(e);
        }
        // 完成
        asyncContext.complete();

    }
}
    

【AsyncServletRejectedHandler】

package com.artisan.handler;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * @author 小工匠
 * @version 1.0
 * @description: 拒绝策略
 * @date 2022/10/6 15:24
 * @mark: show me the code , change the world
 */
@Slf4j
public class AsyncServletRejectedHandler  implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {

        AsyncRequestWrapper wrapper = (AsyncRequestWrapper) r ;
        HttpServletRequest httpServletRequest = (HttpServletRequest) wrapper.getServletRequest();

        try {
            String queryString = httpServletRequest.getQueryString();
            String threadName = wrapper.getThread().getName();
            log.info("当前线程:{} , 当前线程请求参数:{}", threadName, queryString);
            wrapper.getServletResponse().getWriter().println("too many request , current thread:" + threadName + " , current param:" + queryString );
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        // 别忘了 complete
        wrapper.getAsyncContext().complete();
    }
}
    

演示

在这里插入图片描述

2022-10-06 21:30:09.700  INFO 7860 --- [nio-8080-exec-1] com.artisan.servlet.AsyncServlet         : request: i=1, currentThread: http-nio-8080-exec-1
2022-10-06 21:30:11.277  INFO 7860 --- [nio-8080-exec-1] com.artisan.servlet.AsyncServlet         : request: i=2, currentThread: http-nio-8080-exec-1
2022-10-06 21:30:12.813  INFO 7860 --- [nio-8080-exec-1] com.artisan.servlet.AsyncServlet         : request: i=3, currentThread: http-nio-8080-exec-1
2022-10-06 21:30:12.813  INFO 7860 --- [nio-8080-exec-1] c.a.handler.AsyncServletRejectedHandler  : 当前线程:http-nio-8080-exec-1 , 当前线程请求参数:i=3
2022-10-06 21:30:15.355  INFO 7860 --- [nio-8080-exec-1] com.artisan.servlet.AsyncServlet         : request: i=4, currentThread: http-nio-8080-exec-1
2022-10-06 21:30:15.355  INFO 7860 --- [nio-8080-exec-1] c.a.handler.AsyncServletRejectedHandler  : 当前线程:http-nio-8080-exec-1 , 当前线程请求参数:i=4
2022-10-06 21:30:19.712  INFO 7860 --- [c-thread-pool-1] com.artisan.handler.AsyncRequestWrapper  : processFuture 当前处理线程 async-thread-pool-1
2022-10-06 21:30:29.719  INFO 7860 --- [c-thread-pool-1] com.artisan.handler.AsyncRequestWrapper  : processFuture 当前处理线程 async-thread-pool-1

Tomcat 请求处理流程以及异步请求工作原理

在这里插入图片描述

在这里插入图片描述

相关文章
|
2月前
|
前端开发 Java API
利用 Spring WebFlux 技术打造高效非阻塞 API 的完整开发方案与实践技巧
本文介绍了如何使用Spring WebFlux构建高效、可扩展的非阻塞API,涵盖响应式编程核心概念、技术方案设计及具体实现示例,适用于高并发场景下的API开发。
273 0
|
1月前
|
监控 Java API
Spring WebFlux 响应式编程技术详解与实践指南
本文档全面介绍 Spring WebFlux 响应式编程框架的核心概念、架构设计和实际应用。作为 Spring 5 引入的革命性特性,WebFlux 提供了完全的响应式、非阻塞的 Web 开发栈,能够显著提升系统的并发处理能力和资源利用率。本文将深入探讨 Reactor 编程模型、响应式流规范、WebFlux 核心组件以及在实际项目中的最佳实践,帮助开发者构建高性能的响应式应用系统。
321 0
|
7月前
|
前端开发 Java 测试技术
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestParam
本文介绍了 `@RequestParam` 注解的使用方法及其与 `@PathVariable` 的区别。`@RequestParam` 用于从请求中获取参数值(如 GET 请求的 URL 参数或 POST 请求的表单数据),而 `@PathVariable` 用于从 URL 模板中提取参数。文章通过示例代码详细说明了 `@RequestParam` 的常用属性,如 `required` 和 `defaultValue`,并展示了如何用实体类封装大量表单参数以简化处理流程。最后,结合 Postman 测试工具验证了接口的功能。
388 0
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestParam
|
7月前
|
JSON 前端开发 Java
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestBody
`@RequestBody` 是 Spring 框架中的注解,用于将 HTTP 请求体中的 JSON 数据自动映射为 Java 对象。例如,前端通过 POST 请求发送包含 `username` 和 `password` 的 JSON 数据,后端可通过带有 `@RequestBody` 注解的方法参数接收并处理。此注解适用于传递复杂对象的场景,简化了数据解析过程。与表单提交不同,它主要用于接收 JSON 格式的实体数据。
591 0
|
7月前
|
前端开发 Java 微服务
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@PathVariable
`@PathVariable` 是 Spring Boot 中用于从 URL 中提取参数的注解,支持 RESTful 风格接口开发。例如,通过 `@GetMapping(&quot;/user/{id}&quot;)` 可以将 URL 中的 `{id}` 参数自动映射到方法参数中。若参数名不一致,可通过 `@PathVariable(&quot;自定义名&quot;)` 指定绑定关系。此外,还支持多参数占位符,如 `/user/{id}/{name}`,分别映射到方法中的多个参数。运行项目后,访问指定 URL 即可验证参数是否正确接收。
382 0
|
7月前
|
JSON 前端开发 Java
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestMapping
@RequestMapping 是 Spring MVC 中用于请求地址映射的注解,可作用于类或方法上。类级别定义控制器父路径,方法级别进一步指定处理逻辑。常用属性包括 value(请求地址)、method(请求类型,如 GET/POST 等,默认 GET)和 produces(返回内容类型)。例如:`@RequestMapping(value = &quot;/test&quot;, produces = &quot;application/json; charset=UTF-8&quot;)`。此外,针对不同请求方式还有简化注解,如 @GetMapping、@PostMapping 等。
350 0
|
7月前
|
JSON 前端开发 Java
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RestController
本文主要介绍 Spring Boot 中 MVC 开发常用的几个注解及其使用方式,包括 `@RestController`、`@RequestMapping`、`@PathVariable`、`@RequestParam` 和 `@RequestBody`。其中重点讲解了 `@RestController` 注解的构成与特点:它是 `@Controller` 和 `@ResponseBody` 的结合体,适用于返回 JSON 数据的场景。文章还指出,在需要模板渲染(如 Thymeleaf)而非前后端分离的情况下,应使用 `@Controller` 而非 `@RestController`
242 0
|
2月前
|
缓存 Java API
Spring WebFlux 2025 实操指南详解高性能非阻塞 API 开发全流程核心技巧
本指南基于Spring WebFlux 2025最新技术栈,详解如何构建高性能非阻塞API。涵盖环境搭建、响应式数据访问、注解与函数式两种API开发模式、响应式客户端使用、测试方法及性能优化技巧,助你掌握Spring WebFlux全流程开发核心实践。
474 0
|
3月前
|
前端开发 Java API
Spring Cloud Gateway Server Web MVC报错“Unsupported transfer encoding: chunked”解决
本文解析了Spring Cloud Gateway中出现“Unsupported transfer encoding: chunked”错误的原因,指出该问题源于Feign依赖的HTTP客户端与服务端的`chunked`传输编码不兼容,并提供了具体的解决方案。通过规范Feign客户端接口的返回类型,可有效避免该异常,提升系统兼容性与稳定性。
243 0
|
3月前
|
SQL Java 数据库连接
Spring、SpringMVC 与 MyBatis 核心知识点解析
我梳理的这些内容,涵盖了 Spring、SpringMVC 和 MyBatis 的核心知识点。 在 Spring 中,我了解到 IOC 是控制反转,把对象控制权交容器;DI 是依赖注入,有三种实现方式。Bean 有五种作用域,单例 bean 的线程安全问题及自动装配方式也清晰了。事务基于数据库和 AOP,有失效场景和七种传播行为。AOP 是面向切面编程,动态代理有 JDK 和 CGLIB 两种。 SpringMVC 的 11 步执行流程我烂熟于心,还有那些常用注解的用法。 MyBatis 里,#{} 和 ${} 的区别很关键,获取主键、处理字段与属性名不匹配的方法也掌握了。多表查询、动态
127 0