SpringBoot能同时处理多少请求

人间二两风 / 2023-07-18 / 原文

SpringBoot默认的内嵌容器是Tomcat,也就是我们的程序实际上是运行在Tomcat里的。所以与其说SpringBoot可以处理多少请求,到不如说Tomcat可以处理多少请求。

关于Tomcat的默认配置,都在spring-configuration-metadata.json文件中,对应的配置类则是org.springframework.boot.autoconfigure.web.ServerProperties
image

内嵌Tomcat配置

和处理请求数量相关的参数有四个:

server:
  tomcat:
     threads:
       # 最少线程数,默认大小是10。该参数相当于长期工,如果并发请求的数量达不到10,就会依次使用这几个线程去处理请求。
       min-spare: 10
       # 最多线程数,默认大小是200。该参数相当于临时工,如果并发请求的数量在10到200之间,就会使用这些临时工线程进行处理。
       max: 15
     # 最大连接数,默认大小是8192。表示Tomcat可以处理的最大请求数量,超过8192的请求就会被放入到等待队列。
     max-connections: 30
     # 最大等待数,默认大小是100。
     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();
}

如果并发请求数量低于server.tomcat.threads.max,则会被立即处理,超过的部分会先进行等待,如果数量超过max-connections与accept-count之和,则多余的部分则会被直接丢弃。

如何提升 Spring Boot 吞吐量?

  1. 增加内嵌 Tomcat 的最大连接数
  2. 异步执行
  3. 使用 @ComponentScan() 定位扫包
    • 使用 @ComponentScan() 定位扫包比 @SpringBootApplication 扫包更快。
  4. 默认 Tomcat 容器改为 Undertow
    • 默认 Tomcat 容器改为 Undertow(Jboss 下的服务器,Tomcat 吞吐量 5000,Undertow 吞吐量 8000)
  5. 使用 BufferedWriter 进行缓冲
  6. Deferred 方式实现异步调用
  7. 异步调用可以使用 AsyncHandlerInterceptor 进行拦截

延伸:并发问题是如何产生的?

到目前为止,就已经搞明白了SpringBoot可以同时处理多少请求的问题。但是在这里我还想基于上面的例子再延伸一下,就是为什么并发场景下会出现一些值和我们预期的不一样?

Spring容器中的Bean默认是单例的,也就是说,处理请求的Controller、Service实例就只有一份。
在并发场景下,将cookSum定义为全局变量,是所有线程共享的,当一个线程读到了cookSum=29,同时另两个线程也读到是29,三个线程都加1后写回,最终cookSum都变成了30。

private int cookSum = 0;

@GetMapping("/test")
public Response test1(HttpServletRequest request) throws Exception {
    cookSum += 1;
    log.info("做了{}道菜", cookSum);
    Thread.sleep(500);
    return Response.buildSuccess();
}

image

如果处理并发问题

通常我们说的并发:指的是多个线程操作相同的资源,如何保护线程安全,合理的使用资源。

对于一些用户请求,在某些情况下是可能重复发送的,如果是查询类操作并无大碍,但其中有些是涉及写入操作的,一旦重复了,可能会导致很严重的后果,例如交易的接口如果重复请求可能会重复下单。

重复请求的场景有可能是:

  1. 黑客拦截了请求,重放
  2. 前端/客户端因为某些原因请求重复发送了,或者用户在很短的时间内重复点击了。
  3. 网关重发
  4. 网络通信异常或者后端响应慢
    ...

解决方案:

  1. 利用唯一请求编号去重(每次请求,服务端返回客户端一个唯一编号,客户端拿着这个唯一编号去请求)
  2. 业务参数做一个摘要去重(用用户ID、请求URL、请求参数等算一个key,然后MD5(key)来作唯一参数)
  3. 让前端来限制,点击之后,在一定时间内不能再次点击。
  4. 请求去重工具类,Java实现

如何保证并发的安全?

Controller默认是单例的,单例是不安全的,会导致属性重复使用。

  1. 不要在controller中定义成员变量。
  2. 万一必须要定义一个非静态成员变量时候,则通过注解@Scope(“prototype”),将其设置为多例模式。
  3. 在Controller中使用ThreadLocal变量

如何处理高并发问题

高并发就是通过严谨的设计来保证系统能够同时并行处理很多的请求。也就是说系统能够在某一时间段内提供很多请求,但是不会影响系统的性能。

通常我们说的高并发:指的是系统运行过程中,短时间内遇到大量的操作请求的情况,如12306的抢票等。这时候,系统会执行大量的操作(数据库操作,资源请求等),如何处理高并发场景,这个问题以后会总结。

高并发写请求的场景,其中《秒杀抢购》就是最典型的场景。

解决方法:

  • 使用锁的方式,比如分布式锁,也可以利用redis本身操作原子性的特点
  • 写入消息队列,在消息队列中做减库存的操作,做异步校验

在用户下单的时候,用了redis的原子性减库存,如果不支付,一般可以设置一个定时器,定时器时间一到,就把库存加上,同时定义订单失败。

比如1000件商品,系统生成1000个令牌,拿到令牌的用户可以进入消息队列,其他未拿到令牌的直接返回已抢完。