前言
前两天面试的时候,面试官问我:一个 ip 发申请过去,是一个 ip 对应一个线程吗?我忽然愣住了,对于 SpringBoot 如何解决申请如同素来没认真思考过,所以面试完结后就认真钻研了一番,当初就来探讨一下这个问题。
注释
咱们都晓得,SpringBoot 默认的内嵌容器是 Tomcat,也就是咱们的程序实际上是运行在 Tomcat 里的。所以与其说 SpringBoot 能够解决多少申请,到不如说 Tomcat 能够解决多少申请。
对于 Tomcat 的默认配置,都在 spring-configuration-metadata.json 文件中,对应的配置类则是 org.springframework.boot.autoconfigure.web.ServerProperties。
和解决申请数量相干的参数有四个:
server.tomcat.threads.min-spare:起码的工作线程数,默认大小是 10。该参数相当于长期工,如果并发申请的数量达不到 10,就会顺次应用这几个线程去解决申请。
server.tomcat.threads.max:最多的工作线程数,默认大小是 200。该参数相当于临时工,如果并发申请的数量在 10 到 200 之间,就会应用这些临时工线程进行解决。
server.tomcat.max-connections:最大连接数,默认大小是 8192。示意 Tomcat 能够解决的最大申请数量,超过 8192 的申请就会被放入到期待队列。
server.tomcat.accept-count:期待队列的长度,默认大小是 100。
举个例子阐明一下这几个参数之间的关系:
如果把 Tomcat 比作一家饭店的话,那么一个申请其实就相当于一位客人。min-spare 就是厨师 (长期工);max 是厨师总数(长期工 + 临时工);max-connections 就是饭店里的座位数量;accept-count 是门口小板凳的数量。来的客人优先坐到饭店外面,而后厨师开始忙活,如果长期工能够干的完,就让长期工干,如果长期工干不完,就再让临时工干。图中画的厨师一共 15 人,饭店里有 30 个座位,也就是说,如果当初来了 20 个客人,那么就会有 5 集体先在饭店里等着。如果当初来了 35 集体,饭店里坐不下,就会让 5 集体先到门口坐一下。如果来了 50 集体,那么饭店座位 + 门口小板凳一共 40 个,所以就会有 10 人来到。
也就是说,SpringBoot 同时所能解决的最大申请数量是 max-connections+accept-count,超过该数量的申请间接就会被丢掉。
纸上得来终觉浅,绝知此事要躬行。
下面只是实践后果,当初通过一个理论的小例子来演示一下到底是不是这样:
创立一个 SpringBoot 的我的项目,在 application.yml 里配置一下这几个参数,因为默认的数量太大,不好测试,所以配小一点:
server:
tomcat:
threads:
# 起码线程数
min-spare: 10
# 最多线程数
max: 15
# 最大连接数
max-connections: 30
# 最大期待数
accept-count: 10
复制代码
再来写一个简略的接口:
@GetMapping(“/test”)
public Response test1(HttpServletRequest request) throws Exception {
log.info(“ip:{}, 线程:{}”, request.getRemoteAddr(), Thread.currentThread().getName());
Thread.sleep(500);
return Response.buildSuccess();
}
复制代码
代码很简略,只是打印了一下线程名,而后休眠 0.5 秒,这样必定会导致局部申请解决一次性解决不了而进入到期待队列。
而后我用 Apifox 创立了一个测试用例,去模仿 100 个申请:
察看一下测试后果:
从后果中能够看出,因为设置的 max-connections+accept-count 的和是 40,所以有 60 个申请会被抛弃,这和咱们的预期是相符的。因为最大线程是 15,也就是有 25 个申请会先期待,等前 15 个解决完了再解决 15 个,最初在解决 10 个,也就是将 40 个申请分成了 15,15,10 这样三批进行解决。
再从控制台的打印日志能够看到,线程的最大编号是 15,这也印证了后面的想法。
总结一下:如果并发申请数量低于 server.tomcat.threads.max,则会被立刻解决,超过的局部会先进行期待,如果数量超过 max-connections 与 accept-count 之和,则多余的局部则会被间接抛弃。
延长:并发问题是如何产生的
到目前为止,就曾经搞明确了 SpringBoot 能够同时解决多少申请的问题。然而在这里我还想基于下面的例子再延长一下,就是为什么并发场景下会呈现一些值和咱们预期的不一样?
构想有以下场景:厨师们用一个账本记录一共做了多少道菜,每个厨师做完菜都记录一下,每次记录都是将账本上的数字先抄到草稿纸上,计算 x + 1 等于多少,而后将计算的后果写回到账本上。
Spring 容器中的 Bean 默认是单例的,也就是说,解决申请的 Controller、Service 实例就只有一份。在并发场景下,将 cookSum 定义为全局变量,是所有线程共享的,当一个线程读到了 cookSum=20,而后计算,写回前另一个线程也读到是 20,两个线程都加 1 后写回,最终 cookSum 就变成了 21,然而实际上应该是 22,因为加了两次。
private int cookSum = 0;
@GetMapping(“/test”)
public Response test1(HttpServletRequest request) throws Exception {
// 做菜。。。。。。
cookSum += 1;
log.info(“ 做了 {} 道菜 ”, cookSum);
Thread.sleep(500);
return Response.buildSuccess();
}
复制代码
如果要防止这样的状况产生,就波及到加锁的问题了,就不在这里探讨了。