经典面试题:SpringBoot 应用可以同时并发处理多少请求

前言

  • hello,大家好,我是你们的老朋友 Lorin,最近逛帖子看到一个面试题:SpringBoot 应用可以同时并发处理多少请求?看到这个问题大多数朋友也许都会回答 200,这样你也许第二天就会收到如下拒信:
  • 你可能会很难疑惑,面试都答上了为什么没有通过呢?因为这个答案在一定程度上是正确的,但却不是面试官想听到的答案,看完这篇文章,你应该就知道答案了。

构建测试 SpringBoot 应用看看 200 来自哪里

  • 下面我们写一个简单的 demo 测试一把,这里为了压测简单,我们收到请求后直接把线程睡眠2分钟,通过日志打印观察应用能够同时处理的请求数。
java 复制代码
/**
 * @description: 测试 Controller
 * @author: Lorin
 * @create: 2023-08-27 07:46
 **/
@RestController
public class TestController {

    /**
     * 并发测试
     * 接受并发请求,接受请求后睡眠 2 分钟阻塞线程,用于判断实例可以处理多少请求
     *
     * @param num 请求ID 表示已经收到多少个请求
     */
    @GetMapping("/test-with-sleep")
    public void testWithSleep(@RequestParam Integer num) throws InterruptedException {
        System.out.println("接受到请求: " + num);
        Thread.sleep(1000 * 120);
        System.out.println("请求执行完成");
    }

    /**
     * http test with no sleep
     *
     * @param num
     */
    @GetMapping("/test1")
    public void test1(@RequestParam Integer num) {
        System.out.println("接受到请求: " + num);
        System.out.println("请求执行完成");
    }
}

/**
 * @description: 并发测试
 * @author: Lorin
 * @create: 2023-08-27
 **/
class TestControllerTest {

    /**
     * 为了方便测试直接起多线程并发请求测试 当然也可以用 jmeter 等压测工具
     */
    @Test
    void test1() {
        for (int i = 0; i < 1000; i++) {
            int num = i;
            new Thread(() -> {
                try {
                    URL url = new URL("http://localhost:8080/test1?num=" + num);
                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                    connection.connect();
                    System.out.println(connection.getResponseCode());
                    connection.disconnect();
                } catch (Exception exception) {
                    System.out.println(exception);
                }
            }).start();
        }
    }
}

运行结果

  • 你仔细一看200,这不正是我们需要的答案?但是我们作为一个程序员一定要知其然然后知其所以然。

  • 比如面试官会接着问:

    这里并发为什么是200,我们可以调整?
    如果并发数小于200或者大于200是如何表现?

  • 这时候也许就触及到了你的知识盲区了,不要慌马上发车,大家一起来刨根问底来知其所以然:

什么影响了 SpringBoot 应用并发处理请求数

  • 先说结论:应用并发请求数主要由两个因素影响,使用的 Servlet容器(默认使用 Tomcat,常用的还有 jetty、undertow) 和 配置项。
  • 所以在开头的答案我们可以优化一下:在默认配置下,SprigBoot 应用可以并发处理 200 请求。
  • 这就是终极答案吗?不不不,这才是开始,继续发车接下来我们要告诉面试官,不同的 Servlet 容器的工作原理以及配置项,相信下面这些内容回答完,面试官心里的 OS 肯定是:哎哟,这小伙子懂得挺多,不错不错!!

Tomcat

原理

  • 现在我们断点看一下应用的执行流程:
  • 咋一看是不是很眼熟,这不是有点我们线程池的运行逻辑:任务数小于核心数,创建核心线程执行,任务数大于核心线程数,将任务放到任务队列中,任务队列满后创建非核心线程数执行。那么我们来看看几个参数的配置是多少:
vbnet 复制代码
核心线程数:10
非核心线程数:200
任务队列长度:2147483647  Integer.MAX_VALUE
  • 怎么肥事?我们任务队列为 Integer.MAX_VALUE ,我们才 1000 个请求,怎么创建了非核心线程,难道和 Java 的线程池不一样,回到源码中,我们再看看怎么回事:
  • 这个时候我们需要看看任务是如何提交到线程池的,既然都是线程池,那么猜测 提交应该也是一样使用 execute 方法,我们断点试一下发现猜测正确:
arduino 复制代码
org.apache.tomcat.util.threads.ThreadPoolExecutor#execute(java.lang.Runnable)
Java 复制代码
    private void executeInternal(Runnable command) {
        if (command == null) {
            throw new NullPointerException();
        }
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        int c = ctl.get();
        // 判断是否小于核心线程数 小于则创建核心线程处理任务
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true)) {
                return;
            }
            c = ctl.get();
        }
        // 判断任务是否可以放入队列中
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command)) {
                reject(command);
            } else if (workerCountOf(recheck) == 0) {
                addWorker(null, false);
            }
        }
        // 尝试创建非核心线程
        else if (!addWorker(command, false)) {
            reject(command);
        }
    }
    
// 既然没有放入队列中 那么我们看看为什么没有放入队列中
public class TaskQueue extends LinkedBlockingQueue<Runnable> {
    @Override
    public boolean offer(Runnable o) {
      //we can't do any checks
        if (parent==null) {
            return super.offer(o);
        }
        //we are maxed out on threads, simply queue the object
        if (parent.getPoolSize() == parent.getMaximumPoolSize()) {
            return super.offer(o);
        }
        //we have idle threads, just add it to the queue
        if (parent.getSubmittedCount()<=(parent.getPoolSize())) {
            return super.offer(o);
        }
        //if we have less threads than maximum force creation of a new thread
        if (parent.getPoolSize()<parent.getMaximumPoolSize()) {
            return false;
        }
        //if we reached here, we need to add it to the queue
        return super.offer(o);
    }
}

// parent 就是我们的 tomcat 线程池,parent.getPoolSize() 返回当前线程池中的数量
// 关键在这段代码,若当前当前线程数小于最大线程数,则不放入队列中,结合上文逻辑,则进入创建非核心线程逻辑
// if we have less threads than maximum force creation of a new thread
if (parent.getPoolSize()<parent.getMaximumPoolSize()) {
    return false;
}
  • 看完上文的代码豁然开朗,原来 Tomcat 线程池和 Java 线程池的执行逻辑并不一致,Tomcat 线程池会先创建非核心线程,当已创建线程池数量大于最大线程时才会将任务放入队列中。

配置项

  • 看完 Tomcat 的原理,我们来看看 Tomacat 的常用配置是否会影响并发请求的数量,有了上面的基础,我们来看一下常用的配置项:
ini 复制代码
# 非核心线程数
server.tomcat.threads.max=200
# 核心线程数
server.tomcat.threads.min-spare=10

----------------------------------

# 在拒绝连接之前可排队的连接数
server.tomcat.accept-count=100

# 非核心线程空闲时间 1 分钟

# 同时处理最大接收连接数
# maxConnections和accept-count的关系为:当连接数达到最大值maxConnections后,系统会继续接收连接,但不会超过acceptCount的值
# 当该值小于 server.tomcat.threads.max 时会影响最大可并发处理请求数
server.tomcat.max-connections=8192

# 每个连接可以处理的最大请求数
server.tomcat.max-keep-alive-requests=100

# 连接存活时间 默认使用 connection-timeout
server.tomcat.keep-alive-timeout=-1

# 连接超时时间 默认 20s
server.tomcat.connection-timeout=20000
  • 除 server.tomcat.threads.max 会影响并发数以外,其实 server.tomcat.max-connections 也会影响并发请求数,比如将该参数设置为 20:
  • 然后回答上面的补充问题:
diff 复制代码
这里并发为什么是 200,我们可以调整?
- 因为 tomcat 线程池类似 Java 线程池,但有一定区别,会先创建非核心线程后再放入队列中。
- 我们可以通过调整 最大线程数来 控制并发数量

如果并发数小于 200 或者大于 200 是如何表现?(默认配置下)
- 小于 200 会创建核心线程和非核心线程立即处理任务,大于 200 的任务会放入等待队列中

undertow

如何切换 undertow 容器

  • 很简单,我们只需要将原有的 tomcat 容器移除,然后引入 undertow 容器即可
xml 复制代码
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-undertow</artifactId>
        </dependency>

// 观察启动日志可以看到我们已经成功启动
2023-08-27 20:56:29.531  INFO 2618 --- [           main] org.jboss.threads                        : JBoss Threads version 3.1.0.Final
2023-08-27 20:56:29.575  INFO 2618 --- [           main] o.s.b.w.e.undertow.UndertowWebServer     : Undertow started on port(s) 8080 (http)

并发处理请求数

  • 老规矩先并发请求压测一把:
  • 可以看到我们同时并发处理的请求为 64,那么这个 64 是从什么地方的呢,接着往下面看,和上面 Tomcat 的思路一样先断点查看堆栈信息定位:
  • org.jboss.threads.EnhancedQueueExecutor.Builder 找到创建线程池的地方:
  • 可以看到 maxSize、coreSize 来自于 Builder 这个对象,我们回到 Builder 看看这两个参数来自哪里:

  • org.jboss.threads.EnhancedQueueExecutor.Builder

  • 我们看到 maxSize、coreSize 原始值为 64、16,那么是在哪里改变的呢?接着往下断点,我们可以找到:

  • org.xnio.XnioWorker#XnioWorker 中创建了 修改了 Builder 创建了 EnhancedQueueExecutorTaskPool:

  • 那么这里的值又是从哪里来的呢?继续往上找 io.undertow.Undertow#start 发现在这里调用
  • io.undertow.Undertow.Builder#Builder 构建
  • 可以看到默认会根据机器可用的 CPU 数 * 8 计算一个 workerThreads 作为最大线程数和核心线程数,我电脑的 CPU 数为 8,因此最大线程数和核心线程数都为 64。
  • 同时,线程池基于 Java 线程池实现,因此当等待队列未满时使用仅使用核心线程池。

配置项

ini 复制代码
# 线程数 核心线程和非核心线程都使用该值
server.undertow.threads.worker=5

你以为这就结束了?

  • 也许这才是下一个问题的开始,面试官会问你几种 Servlet 容器的优缺点、IO 实现是否一致,以及各自的适用场景、如何进行参数调优等等,后面的内容我们下期见吧。
相关推荐
MadPrinter1 小时前
SpringBoot学习日记 Day11:博客系统核心功能深度开发
java·spring boot·后端·学习·spring·mybatis
dasseinzumtode1 小时前
nestJS 使用ExcelJS 实现数据的excel导出功能
前端·后端·node.js
淦出一番成就1 小时前
Java反序列化接收多种格式日期-JsonDeserialize
java·后端
Java中文社群1 小时前
Hutool被卖半年多了,现状是逆袭还是沉寂?
java·后端
程序员蜗牛2 小时前
9个Spring Boot参数验证高阶技巧,第8,9个代码量直接减半!
后端
yeyong2 小时前
咨询kimi关于设计日志告警功能,还是有启发的
后端
库森学长2 小时前
2025年,你不能错过Spring AI,那个汲取了LangChain灵感的家伙!
后端·openai·ai编程
Java水解2 小时前
Spring Boot 启动流程详解
spring boot·后端
学历真的很重要2 小时前
Claude Code Windows 原生版安装指南
人工智能·windows·后端·语言模型·面试·go
转转技术团队2 小时前
让AI成为你的编程助手:如何高效使用Cursor
后端·cursor