SpringBoot 可以同時處理多少請求?

前言

前兩天面試的時候,面試官問我:一個 ip 發請求過來,是一個 ip 對應一個線程嗎?我突然愣住了,對於 SpringBoot 如何處理請求好像從來沒仔細思考過,所以面試結束後就仔細研究了一番,現在就來探討一下這個問題。

正文

我們都知道,SpringBoot 默認的內嵌容器是 Tomcat,也就是我們的程序實際上是運行在 Tomcat 裏的。所以與其說 SpringBoot 可以處理多少請求,倒不如說 Tomcat 可以處理多少請求。

關於 Tomcat 的默認配置,都在spring-configuration-metadata.json文件中,對應的配置類則是org.springframework.boot.autoconfigure.web.ServerProperties

和處理請求數量相關的參數有四個:

舉個例子說明一下這幾個參數之間的關係:

如果把 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();
}

如果要避免這樣的情況發生,就涉及到加鎖的問題了,就不在這裏討論了。

本文已收錄至我的 Github 倉庫**「DayDayUP」**:github.com/RobodLee/DayDayUP[1],歡迎 Star

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/3J-qTyUXzm2rdYWbfqW5nQ