、并發(fā)與系統(tǒng)設(shè)計考點全拆解)
每年秋招季一過后臺就會收到一堆備考私信。問得最多的其實是同一句話“制造業(yè)大廠的后端崗筆試到底難不難、考什么”我手頭一直留著一套格力2020秋招后端崗筆試題的回憶版當時考完就順手把題目和答案整理成了筆記后來復盤時發(fā)現(xiàn)很多同學掛掉的原因根本不是不會做而是不知道閱卷人到底想考察什么。這篇文章就把整套題的考察邏輯、典型題目、參考答案以及我踩過的坑一次性說清楚。如果你是準備后端崗筆試、尤其是想投制造業(yè)頭部企業(yè)的同學這篇對你應該很有參考價值。先說結(jié)論這套筆試整體偏基礎(chǔ)難度不算“地獄級”但覆蓋面非常廣。它不像互聯(lián)網(wǎng)大廠那樣會把算法題拉到 LeetCode Hard而是更看重 Java 基礎(chǔ)是否扎實、數(shù)據(jù)庫功底是否過硬、并發(fā)場景下能不能寫出靠譜的方案。題型分為選擇題、簡答題、編程題、SQL 題和系統(tǒng)設(shè)計題五類兩個小時題量不小時間分配不合理的人很容易在前面選擇題上磨太久后面編程題反而沒時間寫。1. 整體題型結(jié)構(gòu)與考察邏輯1.1 筆試整體結(jié)構(gòu)與時間分配格力 2020 秋招后端崗筆試一共是 20 道選擇題 5 道簡答題 2 道編程題 2 道 SQL 題 1 道系統(tǒng)設(shè)計題限時 120 分鐘。從題量上就能看出平均單題時間不到 4 分鐘這對審題速度和答題節(jié)奏要求很高。我記得當時考場里不少人前 40 分鐘就耗在選擇題上后面的大題只能潦草寫幾句這基本等于放棄了一半分數(shù)。選擇題里又分單選和多選多選是重災區(qū)因為漏選、錯選都不得分。這類題覆蓋了 Java 集合、并發(fā)、JVM、Spring、MySQL、Redis、網(wǎng)絡(luò)協(xié)議這些后端高頻考點出題風格偏“概念辨析”比如給一段代碼問輸出結(jié)果或者給一個場景判斷哪個方案更合適。簡答題的考察方向比較固定主要集中在并發(fā)編程、JVM 內(nèi)存模型、Spring 核心原理、MySQL 索引和事務隔離級別這五個方向。編程題比較友好一道鏈表操作、一道多線程協(xié)作難度約等于力扣中等偏下更看重你能不能寫出邏輯正確、邊界完整的代碼。SQL 題是訂單表多表聯(lián)查和索引優(yōu)化系統(tǒng)設(shè)計題則是“家電促銷秒殺活動”的高并發(fā)方案設(shè)計這道題最貼合格力的業(yè)務場景也是拉開分數(shù)差距的關(guān)鍵。1.2 各題型占比與得分策略我用一張表把這套題的題型分布、分值占比和推薦用時整理了出來備考階段可以直接按這個比例來訓練。題型題量分值占比推薦用時失分風險點選擇題20題約30%25分鐘多選題漏選、錯選簡答題5題約20%25分鐘只寫結(jié)論不寫原理編程題2題約25%30分鐘邊界條件不完整SQL題2題約15%20分鐘關(guān)聯(lián)條件寫錯、索引不加系統(tǒng)設(shè)計題1題約10%20分鐘沒有分層思路、只說一個方案這個時間分配是我后來復盤時算出來的。選擇題雖然分值不高但它是基礎(chǔ)分的“基本盤”優(yōu)先用排除法快速鎖定答案遇到糾結(jié)的多選先標記、不戀戰(zhàn)。簡答題要按“結(jié)論 原理 舉例”的結(jié)構(gòu)寫比如問“volatile 的作用”不能只答“保證可見性”要把 happens-before、內(nèi)存屏障、禁重排這幾個點都帶上。編程題先寫核心邏輯再補邊界時間不夠也要把思路注釋寫清楚閱卷人通常會給步驟分。系統(tǒng)設(shè)計題要體現(xiàn)分層架構(gòu)從流量入口到數(shù)據(jù)庫逐層拆解哪怕方案不夠完美也要讓閱卷人看到你有全局視野。2. Java基礎(chǔ)與并發(fā)編程核心題目解析2.1 集合框架高頻題HashMap 底層原理這套選擇題里關(guān)于 HashMap 的考察就沒少過而且問得很細。比如“JDK 1.8 中 HashMap 在什么條件下鏈表會轉(zhuǎn)紅黑樹”答案是鏈表長度達到 8 且數(shù)組長度達到 64如果數(shù)組長度不足 64會優(yōu)先擴容而不是轉(zhuǎn)樹。這里很多人只記得“長度達到 8”這一半把“數(shù)組長度達到 64”漏掉多選題剛好就錯在這里。HashMap 的 put 流程也是簡答題的熱門候選先對 key 做 hash 擾動也就是將高 16 位與低 16 位做異或降低哈希碰撞概率然后通過(n - 1) hash定位到數(shù)組下標如果該位置為空直接插入否則遍歷鏈表或紅黑樹存在相同 key 就覆蓋舊值當鏈表長度超過閾值且數(shù)組長度滿足條件時轉(zhuǎn)紅黑樹。擴容時默認容量是 16負載因子 0.75每次擴容為原來的兩倍。我當時把擴容后節(jié)點“高位移動”的規(guī)律寫成了一段注釋因為這一步特別容易疏漏JDK 1.8 擴容后節(jié)點要么留在原位置要么移動到“原位置 舊容量”的位置判斷依據(jù)是新增的那一位 hash 值是 0 還是 1。如果面試/筆試中你能寫出這個細節(jié)閱卷人一般會認為你是真的讀過源碼。還要提一下 ConcurrentHashMap因為并發(fā)場景下它是 HashMap 的替代方案。JDK 1.7 時代用的是分段鎖把數(shù)據(jù)分成多個 Segment每個 Segment 一把鎖JDK 1.8 改成 CAS synchronized 鎖鏈表頭節(jié)點粒度更細。筆試里經(jīng)常拿它和 Hashtable 對比Hashtable 是全局鎖并發(fā)度低ConcurrentHashMap 的并發(fā)度遠高于它。這個對比題不難但很多人把“分段鎖”和“CASsynchronized”混在同一個版本里說這是不準確的。2.2 并發(fā)與JVM經(jīng)典題volatile、synchronized 與線程池參數(shù)簡答題里有一道經(jīng)典題volatile 和 synchronized 的區(qū)別。標準答法是從三個維度展開一是作用volatile 保證可見性和有序性synchronized 保證原子性、可見性和有序性二是使用方式volatile 修飾變量synchronized 修飾方法或代碼塊三是底層實現(xiàn)volatile 通過內(nèi)存屏障和禁止指令重排實現(xiàn)synchronized 通過 Monitor 鎖實現(xiàn)。如果只寫“volatile 是輕量級的 synchronized”這種答案基本拿不到一半分。線程池參數(shù)的考察方式是給一個 ThreadPoolExecutor 構(gòu)造方法讓你寫出 corePoolSize、maximumPoolSize、keepAliveTime、workQueue、threadFactory、RejectedExecutionHandler 分別代表什么然后描述任務提交后的完整執(zhí)行流程。這個流程是核心線程數(shù)未滿時直接創(chuàng)建核心線程執(zhí)行任務核心線程數(shù)滿了任務進入阻塞隊列隊列滿了創(chuàng)建非核心線程執(zhí)行任務非核心線程也滿了觸發(fā)拒絕策略。拒絕策略有四種AbortPolicy 直接拋異常、CallerRunsPolicy 調(diào)用者線程執(zhí)行、DiscardPolicy 靜默丟棄、DiscardOldestPolicy 丟棄隊列頭部的老任務。筆試里容易考的是“隊列用的是無界隊列時maximumPoolSize 參數(shù)是否會生效”——不會因為無界隊列永遠不會滿非核心線程永遠不會被創(chuàng)建。這個坑我當年就踩過。JVM 方面考了一道內(nèi)存區(qū)域劃分程序計數(shù)器、虛擬機棧、本地方法棧、堆、方法區(qū)。其中堆是對象分配的主要區(qū)域方法區(qū)存類元信息、常量、靜態(tài)變量虛擬機棧存局部變量表、操作數(shù)棧、動態(tài)鏈接和方法出口。還順帶考了“哪些區(qū)域會發(fā)生 OutOfMemoryError”堆、方法區(qū)、虛擬機棧都會程序計數(shù)器是唯一不會 OOM 的區(qū)域。G1 收集器的特點也是選擇題??退欠謪^(qū)的、可預測停頓時間的垃圾收集器把堆劃分為多個 Region通過維護優(yōu)先列表來跟蹤回收價值高的 Region。2.3 簡答題實戰(zhàn)模擬與答題模板我把這套筆試里出現(xiàn)過的簡答題整理成了一個小清單建議備考時直接拿來自測每題控制在 5 分鐘內(nèi)寫完描述 volatile 的底層實現(xiàn)原理。解釋 JVM 中類加載的雙親委派機制。說說 Spring IOC 和 AOP 的理解。MySQL 的 InnoDB 為什么用 B 樹而不是 B 樹或紅黑樹如何理解 Redis 的持久化機制RDB 和 AOF 各自優(yōu)缺點答題時我有一個經(jīng)驗不要把簡答題當成“名詞解釋”來寫而要當成“給同事講方案”來寫。比如雙親委派機制先一句話說清定義再說三個類加載器分別是啟動類加載器、擴展類加載器和應用類加載器然后說加載流程是自底向上檢查、自頂向下加載最后強調(diào)這樣做的好處是避免核心類被重復加載和篡改。這樣層次分明閱卷人掃一眼就能抓到踩分點。3. 數(shù)據(jù)庫與Spring生態(tài)題目拆解3.1 SQL題多表聯(lián)查與索引優(yōu)化SQL 題給了一個家電銷售系統(tǒng)的簡化場景三張表用戶表 users、訂單表 orders、商品表 products。第一問是“統(tǒng)計 2020 年 9 月下單次數(shù)最多的前 3 位用戶輸出用戶名和下單次數(shù)”。比較穩(wěn)的寫法是SELECT u.user_name, COUNT(o.id) AS order_cnt FROM users u JOIN orders o ON u.id o.user_id WHERE o.create_time 2020-09-01 00:00:00 AND o.create_time 2020-10-01 00:00:00 GROUP BY u.id, u.user_name ORDER BY order_cnt DESC LIMIT 3;第二問在此基礎(chǔ)上加了難度在訂單表里查“同時購買過商品 A 和商品 B 的用戶 ID”。我的做法是先分別篩出購買過 A 和 B 的用戶集合再取交集SELECT a.user_id FROM ( SELECT DISTINCT user_id FROM orders WHERE product_id A ) a JOIN ( SELECT DISTINCT user_id FROM orders WHERE product_id B ) b ON a.user_id b.user_id;這道題的考點不在 SQL 本身而在你會不會建索引。正確的做法是在 orders 表的(user_id, create_time)上建聯(lián)合索引因為 WHERE 條件里 user_id 用于等值匹配、create_time 用于范圍篩選聯(lián)合索引能同時命中這兩個條件。product_id 單獨建索引即可。如果直接在 create_time 上建單列索引過濾效果會差很多。很多人不寫索引直接交卷這一小問分數(shù)就丟了。3.2 Spring與MyBatis高頻考點Spring 相關(guān)考了 IOC 和 AOP 的概念還深入問到了 Bean 的作用域。單選題里讓你選出“默認作用域”正確答案是 singleton。但題目如果換個問法“哪些作用域下每次獲取都會創(chuàng)建新對象”那就要選 prototype。剩下 request、session、application 都是 Web 容器相關(guān)筆試里出現(xiàn)的頻率相對低一些。另一個高頻點是 Spring 事務的傳播行為尤其 REQUIRED 和 REQUIRES_NEW 的區(qū)別。REQUIRED 是默認值如果當前存在事務則加入當前事務不存在則新建事務。REQUIRES_NEW 是無論如何都新建一個事務外層事務掛起內(nèi)層事務獨立提交或回滾。經(jīng)典的場景是“記錄日志”功能即使主業(yè)務失敗日志也不能丟這時就要用 REQUIRES_NEW。這道題格力筆試的簡答題里出現(xiàn)過我當時答了定義還順手畫了個嵌套調(diào)用的示意圖雖然畫得不標準但把“內(nèi)層事務獨立提交”這個點寫清楚了。MyBatis 考察了#{}和${}的區(qū)別。#{}是預編譯占位符最終通過PreparedStatement的setString等方法傳參能有效防止 SQL 注入${}是字符串拼接直接把值拼接到 SQL 中有注入風險。但${}也不是完全不能用動態(tài)表名、排序字段這類無法用占位符的地方只能用${}關(guān)鍵是必須自己控制輸入值。這個考點如果寫成“盡量用 #{}、避免 ${}”只能算半對要說出${}的正確使用場景才能拿全分。3.3 從若依框架看企業(yè)級項目中的權(quán)限與前端分離這套題雖然沒有直接考若依框架但有一道選擇題涉及了“前后端分離模式下如何進行身份認證”。當時的四個選項里有 Session、Cookie、JWT、OAuth2正確答案是“以上都可以但常見做法是 JWT 或 OAuth2”。這道題其實反映了制造業(yè)技術(shù)棧的一個趨勢——很多企業(yè)級項目已經(jīng)在用若依這類前后端分離框架了。若依后端基于 Spring Boot Spring Security JWT前端用 Vue后端接口通過攔截器校驗 TokenRedis 里存登錄用戶信息和權(quán)限數(shù)據(jù)。備考后端崗時容易被忽略的一點是“前后端分離后后端需要面對跨域問題”。如果你熟悉若依這類框架就會知道后端要通過 CORS 配置或網(wǎng)關(guān)統(tǒng)一處理跨域而不是在每個 Controller 里重復加注解。面試官考察的其實是你的項目經(jīng)驗是真實深入的還是只停留在 Demo 層面。能在筆試里結(jié)合框架談幾個實際場景比如接口鑒權(quán)、跨域處理、驗證碼生成都會讓閱卷人覺得你是真正碰過項目的。4. 編程題與實戰(zhàn)手寫題4.1 鏈表反轉(zhuǎn)的兩種解法編程題第一題是“單鏈表反轉(zhuǎn)”函數(shù)簽名給的是public ListNode reverseList(ListNode head)。這道題算是后端筆試題里的“老朋友”了但每次都能篩掉不少人。我給出的第一種解法是迭代法public ListNode reverseList(ListNode head) { ListNode prev null; ListNode curr head; while (curr ! null) { ListNode next curr.next; curr.next prev; prev curr; curr next; } return prev; }這段代碼的關(guān)鍵是把curr.next先存下來再翻轉(zhuǎn)否則鏈表會斷。很多人第一反應是遞歸寫法遞歸到最后一個節(jié)點然后從后往前翻轉(zhuǎn)指針但遞歸的空間復雜度是 O(n)迭代是 O(1)。筆試閱卷時這兩種都能拿分但如果你能在注釋里寫出“迭代法空間復雜度 O(1)”這個點會顯得更有代碼意識。4.2 Top K 高頻元素優(yōu)先隊列的正確使用編程題第二題是“給定一個非空整數(shù)數(shù)組返回前 K 個高頻元素”。比如數(shù)組是[1,1,1,2,2,3]K2輸出[1,2]。這道題考察的是 Map 堆的組合用法時間復雜度可以做到 O(n log k)。核心代碼如下public int[] topKFrequent(int[] nums, int k) { MapInteger, Integer countMap new HashMap(); for (int num : nums) { countMap.put(num, countMap.getOrDefault(num, 0) 1); } PriorityQueueInteger heap new PriorityQueue( (a, b) - countMap.get(a) - countMap.get(b) ); for (int key : countMap.keySet()) { heap.offer(key); if (heap.size() k) { heap.poll(); } } int[] result new int[k]; for (int i 0; i k; i) { result[i] heap.poll(); } return result; }這里有一個細節(jié)優(yōu)先隊列默認是小頂堆所以用“當堆大小超過 k 時移除堆頂”的策略最后留在堆里的就是頻率最高的 k 個元素。如果誤用大頂堆或者沒控制堆的容量上限復雜度就會退化。筆試環(huán)境里不一定能跑代碼驗證邏輯所以我通常會把 HashMap 的統(tǒng)計流程、堆的容量控制都寫成注釋這樣即使代碼有小問題閱卷人也能看出思路是對的。4.3 多線程協(xié)作三個線程交替打印有一道附加的并發(fā)編程題不是必做但我做了三個線程分別打印 A、B、C循環(huán) 10 次要求輸出順序是 A、B、C、A、B、C……我用的是ReentrantLock配合Condition實現(xiàn)因為Condition可以精準喚醒指定線程比wait/notifyAll可控性高很多。ReentrantLock lock new ReentrantLock(); Condition conditionA lock.newCondition(); Condition conditionB lock.newCondition(); Condition conditionC lock.newCondition(); // 每個線程循環(huán)10次輪到自己的時候打印然后喚醒下一個線程寫這道題時有三個容易扣分的點一是打印完成后必須finally里釋放鎖二是要用while而不是if來判斷是否輪到自己防止虛假喚醒三是三個 Condition 的順序不能搞錯最好用變量維護當前輪次。這類題考察的不只是 API 熟練度更是并發(fā)編程的嚴謹性。5. 系統(tǒng)設(shè)計與實際業(yè)務場景題5.1 家電促銷秒殺活動的高并發(fā)方案這套筆試題的最后一道系統(tǒng)設(shè)計題場景是一個空調(diào)品牌的限時秒殺活動預計瞬間流量是平時流量的幾十倍要求設(shè)計一個可落地的后端方案。這道題非常典型基本就是把電商高并發(fā)場景的核心考點都覆蓋了我當時用了“分層削峰”的思路來答。第一層是前端限制秒殺按鈕點擊一次后立即置灰防止用戶瘋狂點擊配合驗證碼或滑塊拉長請求間隔。第二層是網(wǎng)關(guān)與接口限流用 Redis Lua 實現(xiàn)令牌桶或滑動窗口限流把超過閾值的請求直接返回“排隊中”。第三層是庫存預扣先把商品庫存加載到 Redis下單時用DECR原子扣減庫存扣減成功才允許創(chuàng)建訂單。第四層是異步處理訂單創(chuàng)建的請求丟進消息隊列后端服務異步消費削峰填谷數(shù)據(jù)庫只承受削峰后的寫流量。最后一層才是數(shù)據(jù)庫兜底用樂觀鎖UPDATE ... WHERE stock 0防止超賣同時在訂單表加唯一約束防止重復下單。這道題里最容易踩的坑是“直接把庫存扣減放在數(shù)據(jù)庫里”。如果每秒上萬個請求同時打在主庫上行鎖競爭會讓數(shù)據(jù)庫 CPU 直接打滿。我當時的答題策略是先寫清楚 Redis 預扣庫存的流程再說明 MQ 異步落庫的必要性最后補充了一句“秒殺接口要做接口冪等同一個用戶同一場活動最多只能下單一次”這樣整套方案的閉環(huán)就出來了。5.2 接口冪等與分布式鎖的實現(xiàn)思路系統(tǒng)設(shè)計題里有一道交叉考察的小問秒殺場景下用戶瘋狂點擊導致重復下單怎么從后端保證接口冪等這個問題的標準解法是 token 機制用戶進入秒殺頁面時后端先生成一個唯一 token 存到 Redis用戶提交訂單時必須攜帶這個 token后端先檢查 token 是否存在存在則刪除并繼續(xù)下單不存在則直接返回“請勿重復提交”。因為 Redis 的單線程特性GET DEL可以保證原子性老版本 Redis 需要用 Lua 腳本保證兩步操作原子完成。另一種方案是用數(shù)據(jù)庫唯一鍵兜底比如訂單號直接使用“用戶 ID 活動 ID 時間戳”的拼接結(jié)果在訂單表上建唯一索引重復插入會直接報錯。這個方案實現(xiàn)簡單但依賴數(shù)據(jù)庫不適合作為唯一防線。如果題目繼續(xù)深挖分布式場景還可以用 Redisson 的分布式鎖把“查詢庫存、扣減庫存、創(chuàng)建訂單”這三個操作包在一個鎖里但我那次答題時優(yōu)先寫了 Redis 預扣庫存 唯一鍵約束沒有上分布式鎖因為這套組合對秒殺場景已經(jīng)足夠。5.3 結(jié)合實際業(yè)務智能設(shè)備數(shù)據(jù)上報接口設(shè)計有一道選擇題給了一個場景“空調(diào)設(shè)備每隔 30 秒上報一次運行狀態(tài)后端需要存儲并支持最近 7 天的按小時聚合查詢怎么設(shè)計存儲方案”備選方案里有 MySQL 單表、MySQL 按月分表、HBase、Redis。直覺上很多人會選 HBase因為它適合海量時序數(shù)據(jù)但結(jié)合格力這類企業(yè)的實際場景如果上報量可控、查詢模式固定MySQL 按月分表 定時匯總表也完全夠用而且運維成本低很多。這道題想考察的不是“哪個數(shù)據(jù)庫最強”而是“在給定業(yè)務體量下會不會做技術(shù)選型”。我當時答的時候多寫了一句設(shè)備上報接口本質(zhì)上是一個寫入多、讀取少、實時性要求高的場景可以在應用層加一個內(nèi)存隊列批量寫入數(shù)據(jù)庫減少頻繁的數(shù)據(jù)庫連接開銷同時利用定時任務把明細數(shù)據(jù)聚合成小時維度的匯總表查詢層只查匯總表。這個思路其實和 CEP、時序數(shù)據(jù)庫的思想很接近雖然當時我還不了解這些概念但“聚合 預計算”的套路在后端設(shè)計里是通用的。6. 復盤心得與備考建議6.1 做題時踩過的坑和復盤復盤這套題時我發(fā)現(xiàn)幾個典型的失分點值得單獨提醒。第一個是選擇題里“多選漏選”的問題比如關(guān)于 ConcurrentHashMap 的正確描述里“JDK 1.8 使用分段鎖”和“JDK 1.8 使用 CAS synchronized”這兩項放在一起很多人兩個都選了但前者是 JDK 1.7 的實現(xiàn)正確選項只有一個。這類坑在基礎(chǔ)題里特別常見因為它考察的不是“你知道這個知識點”而是“你知道知識點的版本演進”。第二個坑是 SQL 題沒有寫索引或者寫了但建錯字段。我見過一個同學把索引建在了orders.create_time上但查詢條件是user_id ? AND create_time BETWEEN ? AND ?這種寫法只會在 create_time 上走索引user_id 的過濾是在回表后做的效率遠不如聯(lián)合索引(user_id, create_time)。筆試沒法通過執(zhí)行計劃驗證所以全靠平時的索引優(yōu)化經(jīng)驗。說到底SQL 題不只考語法更考執(zhí)行計劃思維。第三個坑是編程題的邊界條件。鏈表反轉(zhuǎn)那題如果輸入是空鏈表或只有一個節(jié)點代碼要能直接返回空或原鏈表Top K 那題如果 K 大于數(shù)組長度堆的容量邏輯也要兼容。這些邊界條件不用在筆試環(huán)境里跑測試用例但注釋里寫清楚會顯得你經(jīng)驗老到。6.2 針對后端崗筆試的復習路線如果你準備的時間比較緊我建議按“Java 集合與并發(fā) → JVM → MySQL → Spring → 項目場景題”這條線來走和這套筆試的側(cè)重點基本一致。Java 集合重點盯 HashMap、ConcurrentHashMap、ArrayList/LinkedList并發(fā)重點盯 volatile、synchronized、ReentrantLock、線程池、ThreadLocalJVM 重點盯內(nèi)存區(qū)域、類加載、GCMySQL 重點盯索引、事務隔離級別、MVCC、explainSpring 重點盯 IOC、AOP、事務傳播行為、Bean 生命周期。Redis 和消息隊列是加分項但優(yōu)先級沒前面高。如果時間充裕再補一補分布式鎖、緩存一致性、接口冪等這些場景題。最后想說各大論壇上流傳的所謂“標準答案”只能幫你應付選擇題真正拉開差距的其實是簡答題和系統(tǒng)設(shè)計題里體現(xiàn)出的工程思維。拿到題目先別急著寫花 30 秒在草稿紙上列一下答題框架比悶頭寫一堆零散要點要強得多。我當年在系統(tǒng)設(shè)計題里就是把流程圖草稿畫在卷子空白處再把每一個環(huán)節(jié)從前到后串起來寫最后這道題拿到了不錯的分數(shù)。