前言前两天面试的时候,面试官问我:一个请求是从一个ip发出来的,一个ip对应一个thread吗?我顿时愣住了。之前好像没有想过SpringBoot是怎么处理请求的,面试完就仔细研究了一下,现在就来讨论一下这个问题。众所周知,SpringBoot默认的嵌入式容器是Tomcat,也就是我们的程序实际运行在Tomcat中。所以与其说SpringBoot能处理多少请求,不如说Tomcat能处理多少请求。Tomcat默认配置在spring-configuration-metadata.json文件中,对应的配置类为org.springframework.boot.autoconfigure.web.ServerProperties。与处理请求数相关的参数有四个:server.tomcat.threads.min-spare:最小工作线程数,默认大小为10。这个参数相当于一个long-termworker。如果并发请求数没有达到10,就会轮流使用这些线程来处理请求。server.tomcat.threads.max:最大worker线程数,默认大小为200。这个参数相当于temporaryworkers。如果并发请求数在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:#minimumnumberofthreadsmin-spare:10#最大线程数max:15#最大连接数max-connections:30#最大等待数accept-count:10复制代码写个简单的接口:@GetMapping("/test")publicResponsetest1(HttpServletRequestrequest)throwsException{log.info("ip:{},Thread:{}",request.getRemoteAddr(),Thread.currentThread().getName());线程.睡眠(500);返回Response.buildSuccess();}复制代码代码很简单,打印线程名,然后sleep0.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,因为加了两次。私人intcookSum=0;@GetMapping("/test")publicResponsetest1(HttpServletRequestrequest)throwsException{//Cook......cookSum+=1;log.info("我做了{}道菜",cookSum);线程.睡眠(500);返回Response.buildSuccess();锁的问题就没有了,这里就不讨论了。
