
千人并發這四個字放在一起在技術群里能吵出一篇長文。有人說“我單機JVM配個線程池就扛2000”有人說“一到1000系統直接雪崩CPU飆到99%”。我觀察了很久發現兩邊大概率說的根本不是同一件事。有的人測的是“1000個用戶在線”有的人測的是“1000個請求同時打到數據庫”還有的人其實是“1000個線程在JMeter里跑完了”這三者的技術含量天差地別。這篇文章我就從“千人并發算不算大事”這個問題出發把并發數、壓測方法、數據庫鎖、流量治理這些事徹底捋一遍看看1000個并發到底意味著什么壓測要怎么看數據系統會在哪一層先頂不住。1. 先把“千人并發”四個字拆開你測的到底是誰的并發很多爭論吵到最后發現大家連“并發”的定義都沒對齊。這個問題不解決后面所有壓測、調優、架構設計都是空中樓閣。1.1 在線人數并發數容易混這是百分之九十爭論的根源我在很多項目的技術方案評審里見過同一個場景產品經理過來說“我們要支持1000個用戶同時在線技術能做到嗎”開發拍著胸脯說“沒問題1000并發小意思”。結果上線后一到高峰期接口響應從20毫秒漲到2秒數據庫連接池爆掉鏈路監控里全是超時警告。問題出在哪1000個用戶“在線”和1000個“并發請求”是兩個完全不同的概念。在線用戶指的是當前登錄在系統里、處于可用狀態的人數這1000個人不可能每時每刻都在點擊請求。按照二八原則倒推1000個在線用戶里同一秒內真正發起請求的可能只有十幾到幾十個。那什么是真正的并發數嚴格來說是同一時刻系統正在處理中的請求數量也就是 in-flight requests。一個用戶打開一個App頁面前端可能一次性并行發出5個請求這5個請求如果在服務端同時被處理就算5個并發。所以并發數是一個關于“瞬時壓力”的數據跟在線人數之間有換算關系但絕不是等號。1.2 千人并發對應的QPS要按業務形態分別看待搞清楚定義之后還得看業務形態。同樣是“千人并發”在不同場景下對系統的沖擊力完全不一樣。我把常見的業務分成三類第一類是低延遲的讀場景比如商品詳情頁、文章瀏覽。這種請求平均響應時間RT一般在100到300毫秒。根據 Littles Law并發數等于QPS乘以平均響應時間也就是 QPS 并發數 / 平均響應時間。按1000并發、RT200毫秒算QPS大概是5000左右。這個量級對一臺配置合理的服務器來說撐住問題不大只要緩存命中率夠、代碼沒有明顯熱點。第二類是寫場景比如下單、支付回調、庫存扣減。RT通常要500到1000毫秒因為涉及數據庫事務、分布式鎖、外部系統調用。按1000并發、RT1秒算QPS1000。這個量級下壓力會直接傳導到數據庫和中間件尤其是同一行數據被并發更新的時候數據庫層面的鎖沖突會放大RT而RT變長又會讓并發數繼續堆積形成惡性循環。第三類是計算型或長連接場景比如報表導出、AI推理、WebSocket推送。單請求RT可能達到5到10秒QPS只有100到200但線程被長時間占用線程池、內存、連接數全部告急。這種“千人并發”比前兩類難纏得多因為卡脖子資源是線程數和內存加機器都未必好使。1.3 “扛得住”和“響應快”是兩個層面的事還有一個經常被忽略的問題“千人并發”下系統不崩和千人并發下每個用戶都能快速拿到結果這是兩碼事。很多系統在壓測的時候確實能處理完1000個并發請求但看響應時間分布會發現大量請求的響應時間已經超過3秒。如果你不設超時時間這些請求最后也會完成從“系統處理能力”角度看確實抗住了但從用戶體驗看這已經是不可用的狀態了。所以我個人在判斷一個系統能不能扛住千人并發時從來不看“沒崩”這個下限而是看“響應時間達標率”這個上限。就像面試官問“你能獨立負責一個系統嗎”你說“系統不炸就算成功”這跟“我能在業務高峰期保證核心接口P99小于500毫秒”是完全不一樣的回答。后面這句話里隱含了緩存設計、連接池規劃、熔斷降級、全鏈路壓測一整套功夫。2. 用JMeter壓測千人并發線程數1000不代表并發1000聊完定義就到了實操環節。很多人用JMeter壓測千人并發踩進來第一個坑就是在JMeter里設置線程數為1000線程組一啟動瞬間把所有線程都跑滿然后盯著聚合報告看結果。這種做法得到的數據基本沒有參考價值。2.1 壓測前必須想清楚的線程模型先搞明白JMeter的線程組參數在模擬什么。線程數就是模擬的虛擬用戶數Ramp-Up Period是所有線程從0開始到全部啟動完畢的時長循環次數是每個線程跑幾輪請求。拿“千人并發”舉例比較接近真實的做法有這兩種第一種模擬長期的持續壓力。線程數設成1000Ramp-Up Period設為60秒勾選調度器持續時間設為300秒。這樣1000個虛擬用戶在60秒內陸續登場然后持續施壓5分鐘。這更貼近真實場景因為真實用戶不會在同一毫秒集體按下按鈕。第二種模擬瞬時的集中爆發。比如秒殺場景就是要在幾秒內把所有用戶放進來那可以把Ramp-Up Period設成5秒甚至1秒模擬流量瞬間沖頂。但這里有個關鍵認知JMeter里線程數1000不代表系統實際并發就是1000。JMeter線程發出請求后會等響應返回或者超時才會繼續下一次請求或結束。如果服務端處理得很慢RT是2秒而線程發完請求后處于等待狀態那JMeter端的“活躍線程數”是1000但服務端同一時刻真正在處理的請求數可能只有幾百剩下的都在等待響應。要確認服務端的真實并發得去服務端看監控看同時有多少請求在處理中也就是活躍線程數和連接數。2.2 聚合報告里真正要看的三個指標壓測跑完很多人第一時間看聚合報告的Average和Throughput。但我建議先看Error%再看TP99最后才看Average和TPS。這三個指標要聯動著看缺一個都可能得出錯誤結論。先看一個我壓測時經常遇到的場景模擬數據如下指標數值判斷Samples50萬采樣量足夠Average RT180 ms平均數很好看90% Line350 ms還算正常99% Line920 ms已經接近1秒Max RT4800 ms有嚴重長尾Error %0.15%750個請求失敗Throughput2100/s吞吐量數據這個報告單看平均RT180毫秒挺優秀單看TPS2100也不差但Error%是0.15%換算下來50萬個請求里失敗了750個。在千人并發級別的壓測里0.1%的失敗率意味著有用戶真實感知到了報錯。如果這是下單接口750個訂單失敗業務上根本扛不住。所以我的習慣是壓測結果里Error%必須為0或者無限接近0。TP99超過1秒的接口就要開始排查了。2.3 怎么判斷系統的真實并發上限壓測的目的不只是驗證“能不能扛住1000并發”更重要的是找出“最多能扛多少并發”。這里分享一個我自己常用的階梯壓測法從系統當前預估并發的一半起步比如目標是1000就先跑300觀察各項指標穩定后提升到500再觀察再到800、1000、1200。每提升一檔記錄下TPS、RT、Error%、系統資源使用情況。判斷是否觸及上限要同時看三個信號第一TPS是否不再隨并發數增長。如果并發從800加到1000TPS基本持平甚至下降說明系統處理能力到頂了。第二RT是否出現拐點。并發數增長初期RT可能只漲一點點一旦超過某個臨界點RT會急劇上升這是因為隊列堆積了。第三Error%開始從0蹦出數值說明某層組件開始拋異?;虺瑫r。這么壓一圈下來你才能真正回答“千人并發算不算大事”——如果系統在800并發時TPS就開始下降那對于你這個具體系統來說1000并發就是一場災難千真萬確的大事。3. 千人并發最先頂不住的環節數據庫并發鎖CPU、內存不夠了可以加配置線程池不夠了可以調參數但數據庫層的并發問題往往不是加機器就能解決的。千人并發的請求如果穿透了應用層、打到數據庫最容易出事的就兩個地方一個是鎖沖突一個是連接池耗盡。3.1 一條庫存扣減SQL引發的連鎖反應先看一個特別經典的電商場景扣減庫存。最樸素的寫法是這樣的-- 偽代碼邏輯 SELECT stock FROM product WHERE id 100; -- 程序里判斷 stock 0 UPDATE product SET stock stock - 1 WHERE id 100;這種寫法在低并發下沒問題一旦出現1000個并發請求同時去扣同一件商品的庫存就會出大問題。兩個事務可能同時讀到stock1然后都判斷“還有庫存”都執行扣減最后庫存變成-1。很多人第一反應是給查詢加鎖于是改成這樣SELECT stock FROM product WHERE id 100 FOR UPDATE; UPDATE product SET stock stock - 1 WHERE id 100;這個方案確實解決了超賣但引入了新的問題同一行數據的FOR UPDATE鎖會讓1000個并發請求排成一隊。如果每個事務執行要50毫秒1000個請求串行處理最后一個請求要等將近50秒這結果等于系統直接不可用。更合理的方式是使用原子性的條件更新把判斷和扣減合并成一條SQLUPDATE product SET stock stock - 1 WHERE id 100 AND stock 0;執行后判斷影響行數如果是1說明扣減成功是0說明庫存不足。這種寫法讓數據庫在行鎖內完成“檢查庫存和扣減”兩個動作把鎖持有時間壓縮到最短。但注意就算這樣1000個請求仍然要排隊去更新同一行只是每個請求占用行鎖的時間極短整體吞吐能上去不少。現實中的秒殺系統連這一層行鎖都不愿意承受會把庫存預先加載到Redis里做前置扣減真正落庫時再異步對賬。千人并發的庫存扣減純靠數據庫扛已經非常吃力這是業務場景決定了必須上額外組件。3.2 更隱蔽的殺手數據庫連接池耗盡比起行鎖沖突連接池耗盡在千人并發下更常見也更隱蔽。因為鎖沖突會直接反映在慢SQL上比較容易發現而連接池耗盡的表現多種多樣可能是應用日志里出現“Connection is not available, request timed out”、可能是某個接口突然變慢、也可能是數據庫負載看著不高但應用整體像死了一樣。線程池和連接池的關系經常被混淆。一個經典配置錯誤是Tomcat的max-threads設成500但HikariCP的maximum-pool-size才20。當500個請求線程同時進來只有20個能拿到數據庫連接剩下480個全堵在“獲取連接”的等待隊列里。這些等待線程占著Tomcat線程池不釋放新請求進不來應用表現為整體不可用但數據庫其實閑得很。所以配置連接池之前先理解這個等式最大并發數據庫請求數 應用最大線程數 × 單線程串行請求數。如果應用線程池是500連接池至少得能覆蓋大部分線程的并發數據庫訪問量。我一般建議先粗調為Tomcat線程數的四分之一到三分之一比如Tomcat線程數是200HikariCP的maximum-pool-size可以先設成50然后通過壓測觀察活躍連接數再微調。參數上HikariCP有一個minimum-idle和maximum-pool-size。壓力場景下建議把minimum-idle和maximum-pool-size設為相同值省去連接動態創建的延遲。連接池不是越大越好因為數據庫的CPU核數和內存決定了它真正能同時執行的SQL數量連接太多反而增加上下文切換成本。MySQL一般建議連接數控制在500以內HikariCP默認值10對這個場景來說太小要根據壓力測試結果上調。3.3 熱點行更新的對沖思路千人并發下如果都盯著同一行更新不管怎么調優單行數據庫更新的天花板就擺在那里。這里有兩個思路可以借鑒一是拆分熱點。比如庫存扣減不要只在一個字段上扣可以按ID取模拆成多個庫存子記錄每個子記錄獨立扣減匯總時再加總。這本質上是把單行的行鎖競爭分攤到多行上。二是把熱點請求串行化。用Redis的去重隊列或分布式鎖讓同一商品的扣減請求在應用層排成一個隊列依次處理。1000個并發請求在Redis隊列里排隊進入應用應用每次只處理一個既避免了數據庫行鎖又因為Redis單線程特性天然有序邏輯還更簡單。代價是你引入了新的組件、新的故障點。千人并發可以把數據庫層打穿這些事情如果不提前設計壓測的時候就會直接暴露出來。早點通過壓測把瓶頸炸出來比上線后被用戶炸要好得多。4. 流量治理不是擺設Sentinel在千人并發下的真實角色千人并發的流量一旦進來最怕的不是請求多而是流量沒有節制——系統本來能扛500并發突然來了2000結果就是雪崩。這個時候流量治理的那套東西就開始起作用了。很多人把Sentinel僅僅理解成“限流工具”有點低估它。4.1 流控、熔斷、系統保護三件事各有各的職責Sentinel的三大核心能力分別是流量控制、熔斷降級和系統自適應保護。我在這幾年的實踐中確實體會到了這三者分工的不同。流量控制解決的是“入口流量太大”的問題。比如接口只支持1000 QPS超過的就直接返回“系統繁忙”。這不是為了拒絕用戶而是為了保住系統的其他部分不至于全掛。熔斷降級解決的是“下游已經壞了別再往里打流量”的問題。比如一個接口依賴了第三方慢接口正常RT是200毫秒現在變成10秒了。如果繼續放流量進來應用線程會被全部占滿。熔斷器在連續失敗率達到閾值后打開直接短路請求給下游恢復的時間。系統自適應保護解決的是“別把整臺機器干死”的問題。Sentinel會根據系統當前的Load、CPU使用率、平均RT這些指標動態地調整入口流量。喝咖啡類比就是杯子就那么大咖啡倒得太快會溢出來系統保護就是那個自動放慢倒水速度的裝置。這三種能力在千人并發場景下的配合是流量控制管入口熔斷降級管調用鏈系統保護兜底保命。4.2 一個能直接照抄的QPS限流規則配置以Spring Cloud Alibaba Sentinel為例最簡單的限流規則可以這樣定義[ { resource: POST:/api/order/create, count: 1000, grade: 1, limitApp: default, strategy: 0, controlBehavior: 0 } ]這段配置的含義是針對/api/order/create這個資源QPS超過1000的時候開始限流超過的請求直接拒絕。grade1代表按QPS維度限流strategy0表示直接拒絕controlBehavior0是默認的快速失敗策略。但這里有個很容易踩的坑count設成1000不代表你系統就能扛1000 QPS。這個數字應該來自壓測結果而不是拍腦袋。如果你的壓測數據顯示這個接口在600 QPS的時候RT已經開始快速上升那count就設600留出30%到50%的余量放在生產環境。線上流量是突發的不像壓測那樣線性增長所以閾值留余量是保命的習慣。另外一個被很多人忽略的點是控制行為。默認的快速失敗對突發流量并不友好突刺流量過了之后系統會有一段時間的“空窗期”因為大量請求被拒了。更平滑的做法是用冷啟動或勻速排隊模式。勻速排隊適合削峰填谷的場景比如每個請求在隊列里等最多10毫秒超過就拒絕。對秒殺這類場景非常有用。4.3 我對Sentinel的觀察限流攔得住流量攔不住爛代碼聊到這里想說一句實踐中的體會Sentinel這類流量治理組件是“保險絲”不是“發動機”。它能在流量超載時保護系統不被打死但如果你系統本身在500 QPS時RT就飆到3秒上了Sentinel只是讓它在500 QPS時開始報錯而不是讓系統能扛住1000并發。我見過不少團隊的做法是系統壓測不過就加限流。把閾值調低壓測就過了。但業務指標擺在那里用戶量增長一點限流就開始拒絕用戶了。這其實是把問題往后推并沒有解決系統本身的性能瓶頸。所以正確姿勢是先通過壓測和排查把系統本身的性能做到位再用Sentinel做最后的流量兜底。千人并發下系統能不能扛6成靠代碼質量和緩存設計3成靠連接池和線程池的配置最后1成才是Sentinel這種保命裝置。順序反了治理組件反而會成為你掩蓋問題的工具。5. 并發和并行、線程數調優Java并發里容易翻車的細節千人并發的另一個側面是把請求“并發地”處理。很多Java后端項目的并發問題根源不在框架而在對并發編程基礎的理解偏差。5.1 并發與并行不是一回事它在高并發場景下的意義并發是指系統能夠同時處理多件事并行是指系統在同一時刻真的同時執行多件事。這里的區別我用喝酒來說一個人在四張桌子之間來回倒酒一個時間點只能處理一張桌子這叫并發四個服務員同時各服務一張桌子這叫并行。在單核CPU機器上程序本質上沒法真正并行只能通過時間片切換制造“并發”的假象。多核機器上多線程才能真并行。這也解釋了為什么很多并發問題在研發環境的4核機器上測不出來一上生產8核甚至16核的機器又出現數據錯亂因為并行度高并發競爭變嚴重了。Java并發里最經典的誤區就是遇到性能問題就加大線程池。但線程不是越多越好因為CPU時間片就那么多線程超過核心數之后大部分時間都花在線程切換上。操作系統在線程切換時要保存和恢復上下文這個操作的CPU開銷不小。當一個線程池里有上千個活躍線程時光是上下文切換就能吃掉幾十個核心的算力。5.2 線程池大小怎么估別套公式先分場景網上流傳一個線程池大小公式線程數 CPU核數 × (1 等待時間 / 計算時間)。這個公式有一定參考價值但很多人用錯了場景。公式背后的邏輯是如果線程大部分時間在IO等待那在同一時刻可以多放一些線程來“填補”等待空隙。比如計算時間50毫秒等待時間950毫秒每個線程有95%的時間在等那理論上可以多加近20倍的線程來壓滿CPU。但真實場景里等待時間不是固定值而是隨并發數變化。因為數據庫連接池排隊、第三方接口響應變慢都會讓等待時間變長。所以公式只能作為起點最終值還是要壓測來定。我最近的實操習慣是對IO密集型的業務大部分后端接口都算線程數先設為 (CPU核數 × 2) 然后用壓測結果校準。而不是一上來就設1000。一個1000線程的Tomcat如果數據庫連接池只有50大部分線程都在傻等反而把內存和CPU耗在無用的調度上。5.3 鎖、原子類、線程封閉各自處理不同層面的并發問題Java并發領域的三板斧synchronized/ReentrantLock管互斥CAS原子類管單變量更新ThreadLocal管線程封閉。它們的使用邊界很清晰。synchronized適合臨界區代碼塊較長的場景JVM會做偏向鎖、輕量級鎖的自動優化如果只是對一個計數做遞增用synchronized就太重了LongAdder或AtomicInteger更合適CAS在低競爭下性能很好如果希望每個線程有自己的副本、互不干擾就用ThreadLocal。千人并發場景下最容易出的問題是用錯了工具。比如樂觀鎖CAS適合沖突低的場景一旦1000個線程同時對一個AtomicLong做incrementAndGetCAS會大量自旋重試性能可能反而比synchronized差。這就是為什么LongAdder在并發度高時性能更好——它把計數分散到多個槽位減少沖突。線程池的拒絕策略也是一樣ThreadPoolExecutor默認的AbortPolicy在任務滿了之后直接拋異常這在千人并發下會把錯誤堆棧打滿。實際生產里我更推薦CallerRunsPolicy任務滿了讓提交任務的那個線程自己執行把壓力逆向傳導給調用方相當于一個自然的背壓機制。6. 從一千到一萬容量估算和預警水位不能靠拍腦袋很多系統一開始的目標就是千人并發但業務發展起來后就會變成五千人、一萬人。如果從第一天起就把容量估算的模型建好后面擴容和改造都有據可依。6.1 從業務指標反推技術指標一個可以抄的計算過程容量估算最忌諱張嘴就說“我們要支持100萬用戶”。技術指標必須從業務指標一步步倒推出來中間每一步的假設都要明確記錄后續才能根據真實數據去校準。我拿一個典型的B端管理后臺來演算。目標支持10萬注冊用戶日活按照10%估算就是1萬人。假設每個活躍用戶一天觸發50個請求那一整天的請求總量就是50萬。除以86400秒得到平均QPS大約是5.8峰值按平均值的5倍估算峰值QPS大約30——這量級壓根不叫壓力。但換個場景同樣是10萬用戶如果是課件搶購秒殺系統10萬人同時點搶購按鈕這個瞬時流量就不能按平均值算了。按“10萬用戶中10%的人擠在同一秒”來估算瞬時QPS就有1萬。對接口的要求就完全變了。千人并發在普通業務系統算中高壓力在秒殺場景里只能算一個數據點。這是估算的第一步先明確業務模型再談QPS。第二步是用Littles Law把QPS轉成并發數。假設你的接口平均RT是200毫秒峰值QPS是5000那平均并發數就是5000×0.21000。正好是題目里的1000并發。也就是說如果RT不變5000 QPS和1000并發是一回事。這串換算在你做容量規劃的時候非常有用。6.2 千人并發場景下的預警水位建議系統上線前一定要把監控指標和預警水位提前配好不然等線上出問題再查監控黃花菜都涼了。千人并發場景下我建議重點盯這幾個指標和水位監控對象預警水位說明CPU使用率連續5分鐘超過70%如果長期75%以上壓測能力會明顯下降內存使用率持續超過80%排查GC頻率和內存泄漏Tomcat活躍線程數接近max-threads的80%說明請求在排隊RT會快速惡化數據庫連接池活躍數達到maximum-pool-size的80%大概率有連接泄漏或者慢SQL數據庫慢SQL數單分鐘超過10條慢SQL會拖垮CPU和連接池接口P99 RT超過500毫秒P99比平均值更敏感地反映用戶體驗這些水位不是死的但可以作為初始模板每個項目根據壓測結果去調整。重點是預警水位必須在壓測階段就確定下來而不是上線后再猜。6.3 一千并發到一萬并發架構演進的關鍵分岔路最后聊聊演進。1000并發的時候你可能覺得只要加機器就行但到了5000并發很多問題會因為規模而質變。第一步是先做單機極致優化緩存熱點數據、優化SQL索引、調整連接池和線程池參數、開啟HTTP壓縮。這一步能讓單機能力從200并發提到800甚至1000。第二步才是加機器做水平擴展。但水平擴展有一堆前置條件無狀態應用、會話外置、緩存集群化、數據庫從庫拆分。很多系統在1000并發時能跑是因為應用狀態都藏在本地內存里一旦水平擴展到多臺Redis、分布式鎖、消息隊列這些基礎設施就成了新的短板。第三步是異步化和削峰填谷。很多同步處理邏輯在1000并發下還能硬扛但到5000并發就扛不住了。把核心鏈路里的非關鍵步驟比如消息通知、積分結算、日志落庫扔到MQ里異步處理系統的同步壓力會小很多。每次架構演進都是一次新的壓測循環估算、壓測、優化、升級。這么一輪輪跑下來你才會慢慢形成對“千人并發”這種問題的手感。7. 聊聊我自己的體感做了這么久后端和架構設計我對“千人并發算不算個大事”這個問題已經有了一個自己的答案不說算也不算不算。說它不算是因為這確實是一個可以量化、可以通過壓測驗證、可以通過架構設計解決的問題。它不是那種懸而未決的科學難題1000并發就是1000并發測出來的數據不會騙人。說它算是因為很多團隊在真正面對1000并發的過程中才發現問題根本不在“1000”這個數字而在于你花了多久時間發現系統在哪一層先扛不住又花了多久去優化那一層。我見過太多項目死在“覺得1000并發不難”這件事上。代碼寫得隨意、SQL沒有explain過、線程池參數保持默認、緩存策略靠猜——等到壓測的時候問題的復雜度才開始爆發。反過來如果你能把壓測和監控當成日常習慣1000并發確實又是一道不算太難的坎。最后分享一個我的個人偏好這種問題不要去網上爭也不要去聽別人吹牛。自己搭個環境壓一遍把TPS、RT、Error%、系統資源這幾個數字打出來你看一眼就明白自己的系統離1000并發還差多遠。數字不會騙人比任何經驗之談都管用。