編程到系統(tǒng)設(shè)計)
1. 面試流程與整體定位途游游戲在北京的游戲圈里算比較務(wù)實的那一類做的是棋牌和休閑游戲賽道技術(shù)上對后端的實時性、穩(wěn)定性和高并發(fā)要求都不低。我這次投的是后端開發(fā)崗整體面試走下來最大的感受是他們不怎么看八股文的記憶能力更在意你面對真實業(yè)務(wù)場景時能不能把技術(shù)用對地方。先說下面試流程總共四輪加一輪HR溝通。第一輪是電話初篩大概二十分鐘主要確認你目前在哪個城市、離職狀態(tài)、工作年限、做過什么類型的項目問了一個簡單的技術(shù)問題——大概是Java里HashMap在并發(fā)場景下會有什么問題屬于熱身級別回答清楚就能過。第二輪是技術(shù)面一個多小時面試官是后端組的核心開發(fā)。這一輪問得最細從Java并發(fā)、網(wǎng)絡(luò)編程到Redis、MySQL都有覆蓋中間穿插了兩個代碼題一個偏數(shù)據(jù)結(jié)構(gòu)一個偏游戲業(yè)務(wù)場景設(shè)計。第三輪是技術(shù)終面面試官應該是技術(shù)負責人級別問題更宏觀比如如果讓你設(shè)計一個跨服排行榜系統(tǒng)你會怎么做同時會對簡歷上的項目做非常深度的追問尤其是線上故障排查和性能優(yōu)化這塊。第四輪是HR面聊薪資、到崗時間、之前的工作經(jīng)歷和離職原因沒有太多技術(shù)內(nèi)容。從我準備的感受來說游戲后端面試和互聯(lián)網(wǎng)業(yè)務(wù)后端面試有一個明顯的區(qū)別業(yè)務(wù)后端重點在看你對Spring生態(tài)的熟悉程度而游戲后端更看重并發(fā)編程、網(wǎng)絡(luò)通信、數(shù)據(jù)結(jié)構(gòu)和架構(gòu)設(shè)計。途游這家公司技術(shù)棧里Java占比很高Netty用得比較重Redis和MySQL是標配所以要重點準備的方向一目了然。如果你也想投這家公司我的建議是要在簡歷里主動把游戲相關(guān)的項目經(jīng)驗放前面哪怕只是個人練手項目比如用Netty寫過一個簡單的游戲服務(wù)器框架或者做過一個排行榜的Redis方案這類內(nèi)容在面試官眼里比一堆CRUD接口值錢得多。2. 為什么游戲后端面試和互聯(lián)網(wǎng)后端不一樣很多從業(yè)務(wù)后端轉(zhuǎn)游戲后端的同學上來最懵的一件事是面試官根本不按SpringBoot那套聊天。網(wǎng)上有個很熱的問題叫Java SpringBoot項目后端可以直接上手改代碼嗎放在游戲后端這個場景里答案往往是能改但主心骨不在SpringBoot上。游戲后端的技術(shù)重心和業(yè)務(wù)后端差異很大。業(yè)務(wù)后端核心是接口設(shè)計、權(quán)限控制、事務(wù)管理和數(shù)據(jù)流轉(zhuǎn)框架是骨架SpringBoot選對了業(yè)務(wù)代碼往里面填就行。游戲后端就不一樣核心在實時交互玩家操作要毫秒級響應服務(wù)器要同時扛住幾萬甚至幾十萬人的并發(fā)在線。所以面試官聊的都是Netty的線程模型、自定義協(xié)議怎么設(shè)計、消息怎么廣播、狀態(tài)同步還是幀同步、Redis在排行榜和緩存里怎么用不踩坑。后端開發(fā)除了增刪改查還有什么這個問題放在游戲后端里答案特別豐富。我在面試中被問到過一個很典型的場景題設(shè)計一個全區(qū)服排行榜要能實時更新、要支持海量玩家、還要能查排名區(qū)間。這個需求核心不在數(shù)據(jù)庫的CRUD而在數(shù)據(jù)結(jié)構(gòu)選型和存儲方案設(shè)計。你用什么結(jié)構(gòu)存儲分數(shù)ZSet當然可以但ZSet單節(jié)點內(nèi)存夠不夠要不要分片排行榜是按天重置還是跨天累計同分怎么排名這些問題的深度已經(jīng)遠超SQL語句本身了。所以準備游戲后端面試思維要先轉(zhuǎn)換過來。不要一上來就刷Spring的源碼要把時間花在并發(fā)編程、網(wǎng)絡(luò)協(xié)議、數(shù)據(jù)結(jié)構(gòu)和系統(tǒng)設(shè)計這些更底層的硬功夫上。不是SpringBoot沒用而是游戲服務(wù)器往往是長連接通信業(yè)務(wù)邏輯在自定義協(xié)議層就開始處理了SpringBoot更多是負責管理后臺和日志監(jiān)控這類輔助模塊。這里還要提一個容易踩的坑簡歷上千萬不要把游戲后端經(jīng)驗寫成基于SpringBoot的某某系統(tǒng)面試官一看就覺得你做的不是真正意義上的游戲服務(wù)器。哪怕你確實用了SpringBoot管理后臺也要把通信層、邏輯層、存儲層分開寫清楚重點突出Netty和自研協(xié)議的部分。3. 技術(shù)知識考察全拆解3.1 Java并發(fā)編程是重頭戲途游的第二輪技術(shù)面Java并發(fā)這塊問得非常細不是問你synchronized和ReentrantLock有什么區(qū)別這種表面題而是直接丟給你一個場景一臺游戲服務(wù)器要支持兩萬玩家同時在線玩家之間會發(fā)生大量的異步消息交互你怎么設(shè)計線程模型這個問題背后考的是線程池參數(shù)設(shè)置、任務(wù)隊列的選擇和線程安全的數(shù)據(jù)結(jié)構(gòu)。我當時回答的思路是先分析場景特點游戲消息的特點是短、頻、快每條任務(wù)的執(zhí)行時間可能只有幾毫秒但數(shù)量非常大而且不同玩家之間的消息不允許互相阻塞。基于這個特點線程池的核心線程數(shù)和最大線程數(shù)不適合設(shè)置得太大隊列要選擇有界隊列拒絕策略要配合消息重發(fā)機制而不是簡單丟棄。面試官追問道如果某個玩家的消息處理邏輯里不小心寫了一塊耗時很長的數(shù)據(jù)庫操作導致線程池隊列堆積你怎么辦這個問題問得好因為真實業(yè)務(wù)里這種問題太常見了。我的回答是把耗時操作異步化用CompletableFuture或者消息隊列把DB操作扔到單獨的線程池里執(zhí)行游戲邏輯線程只做內(nèi)存操作和狀態(tài)變更DB落盤走異步路徑。他還問了ConcurrentHashMap在什么場景下會出現(xiàn)線程安全問題這個很多人會答錯。ConcurrentHashMap的單個操作是線程安全的但復合操作不是比如先get再put這種讀改寫操作在并發(fā)場景下結(jié)果可能不符合預期。游戲場景里典型的例子是玩家充值后發(fā)放道具先查余額、再扣款、再發(fā)道具這三個操作必須保證原子性否則并發(fā)下可能發(fā)兩次道具。這個問題我答到了用CAS循環(huán)或者分布式鎖來解決面試官明顯比較滿意。3.2 Netty與網(wǎng)絡(luò)協(xié)議設(shè)計游戲后端的地基第二輪面試有一半時間花在Netty上這也是游戲后端和業(yè)務(wù)后端最大的分水嶺。面試官先讓我講Netty的Reactor線程模型這個屬于基礎(chǔ)但一定要講出層次。我回答了Boss Group和Worker Group的分工Boss線程負責Accept連接Worker線程負責處理讀寫事件每個Worker線程綁定一個Selector多個Channel注冊到同一個Selector上。接下來問的是自定義協(xié)議設(shè)計。游戲里客戶端和服務(wù)器之間的通信不能像HTTP那樣一個請求一個響應通常是一條TCP長連接上跑很多消息所以必須有自己的一套消息格式。我當時設(shè)計的是類似這樣的結(jié)構(gòu)消息頭四個字節(jié)表示包體長度兩個字節(jié)表示消息ID一個字節(jié)表示協(xié)議版本后面跟著用Protobuf序列化的包體數(shù)據(jù)。面試官接著問如果客戶端發(fā)過來的消息長度字段和實際包體不一致你怎么處理這就是經(jīng)典的粘包拆包問題答案是長度字段在解碼時做合法性校驗超出合理范圍直接斷開連接防止惡意報文攻擊服務(wù)器。他還追問了半包問題即一條消息被拆成了多個TCP分片怎么辦。這個就是Netty的ByteToMessageDecoder做的事情通過累積緩沖等讀夠一個完整包再交給業(yè)務(wù)邏輯處理。我補充了一個真實場景里的經(jīng)驗不能用InputStream直接read因為read一個字節(jié)就返回一個字節(jié)的話函數(shù)調(diào)用開銷太大必須要用批量累積再拆包的方式。Netty這關(guān)考完之后我能明顯感覺到面試官是在驗證你到底是用過Netty還是理解了Netty。兩者的區(qū)別在于你遇到真正的高并發(fā)連接時能不能解釋清楚為什么Netty比傳統(tǒng)的BIO模型性能高一個數(shù)量級。3.3 Redis在游戲業(yè)務(wù)里的深度應用Redis在游戲后端里扮演的角色比一般業(yè)務(wù)系統(tǒng)要重得多因為排行榜、抽獎、簽到、活動配置、熱點數(shù)據(jù)緩存全都要靠它。面試官針對Redis的提問非常貼合游戲場景。最典型的問題是排行榜。我問到的是ZSet的應用比如斗地主玩家的積分排行。ZSet能保證分數(shù)有序但是有一個坑如果積分相同是按照成員名稱的字典序排列的這不一定符合業(yè)務(wù)需求。我自己踩過這個坑所以主動提了解決方案把積分一個足夠隨機的序列號拼成一個復合分值保證同分時順序也不至于太死板或者直接用時間戳參與排序邏輯。面試官聽完點頭認可還問了一個延伸場景如果排行榜要按賽季重置怎么辦方案是用兩個Key當前賽季的Key和上賽季的Key賽季結(jié)束時先把當前Key的數(shù)據(jù)歸檔再啟用一個新的Key。另一個問得比較多的是緩存一致性。游戲里玩家信息、背包物品是高頻訪問的數(shù)據(jù)不能每次都查數(shù)據(jù)庫但是緩存和數(shù)據(jù)庫之間怎么保證一致性是經(jīng)典難題。我給出的方案是Cache Aside模式讀請求先查緩存緩存不存在再查DB并回填寫請求先更新DB再刪除緩存。面試官問了一個很尖銳的問題如果你更新了DB還沒來得及刪緩存這時候有讀請求過來讀到的還是舊數(shù)據(jù)怎么辦我的回答是加一個短TTL做兜底比如緩存設(shè)置8秒過期極端情況下最多有8秒的臟讀窗口在游戲業(yè)務(wù)里可以接受。Redis持久化策略也被問了游戲里掉數(shù)據(jù)是重大事故。RDB適合做冷備和快速恢復AOF則能保證更強的數(shù)據(jù)可靠性建議同時開啟AOF刷盤策略用everysec平衡性能和可靠性。這個回答比較常規(guī)但面試官額外問了一句AOF文件越來越大會不會影響性能我知道他想要的是AOF重寫機制所以補充了Rewrite相關(guān)思路。3.4 數(shù)據(jù)庫與分庫分表玩家數(shù)據(jù)怎么存游戲數(shù)據(jù)庫設(shè)計和傳統(tǒng)業(yè)務(wù)數(shù)據(jù)庫設(shè)計的最大區(qū)別在于數(shù)據(jù)模型的拆分邏輯。途游的面試官問的是幾百萬注冊玩家的數(shù)據(jù)你怎么設(shè)計存儲我的方案是玩家ID做分片鍵。按照玩家ID哈希取模分庫比如分成16個庫每個庫再按ID段分表。這樣的好處是同一個玩家的所有數(shù)據(jù)都落在同一張表里不需要跨庫查詢。面試官追問跨服玩法怎么辦比如全服天梯榜不可能是分片后每片獨立排名因為排名是全局的。我回答了用Redis ZSet維護全局榜單DB只做異步落庫這樣讀性能高寫壓力也分散。還有一個有意思的問題玩家身上動輒幾百個字段比如金幣、道具、成就、任務(wù)進度是一張超寬表還是多張窄表我的答案是寬表加JSON字段混合使用核心數(shù)值字段要單獨建列方便查詢和加索引低頻和結(jié)構(gòu)多變的字段用JSON存。這個方案在面試中得到了認可因為游戲玩家的字段變化太頻繁如果每次加一個道具類型就要做一次ALTER TABLE開發(fā)效率會非常低。MySQL這塊還被問到主從延遲如何處理。游戲里玩家充值后立刻查余額因為主從復制延遲可能查到舊值。我的方案是強制讀主庫或者提供一個寫后讀一致的標記玩家寫完數(shù)據(jù)后一定時間內(nèi)的讀請求都走主庫。面試官又追問了死鎖問題我用一個具體案例說明兩個玩家同時進行道具交換一條SQL先更新玩家A再更新玩家B另一條SQL先更新玩家B再更新玩家A如果并發(fā)執(zhí)行就會死鎖。解決方案是按照玩家ID排序后再依次更新保證鎖順序一致。4. 項目深挖與線上排查實錄第三輪面試是項目深挖這輪是最容易翻車也是最能拉開差距的一輪。面試官會拿著你簡歷上的項目逐行追問細節(jié)如果你只是參與過某個項目但對核心技術(shù)點講不出所以然很快就會被識破。我簡歷上寫了一個用Netty實現(xiàn)的游戲服務(wù)器框架面試官針對它問了三個問題。第一個問題是客戶端斷線重連后服務(wù)器上的玩家狀態(tài)應該怎么恢復這個問題的核心在于內(nèi)存狀態(tài)和持久化狀態(tài)如何同步。我的設(shè)計是玩家上線時加載全部數(shù)據(jù)到內(nèi)存游戲過程中所有操作都直接改內(nèi)存同時通過異步任務(wù)定期把臟數(shù)據(jù)落庫。斷線時TCP連接斷開不立即清理內(nèi)存中的玩家對象而是保留一段合理時間比如60秒。如果玩家在這段時間內(nèi)重連直接復用內(nèi)存對象恢復速度極快。超過時間才會清除內(nèi)存并落庫歸檔。面試官很認可這個方案因為省去了每次斷線都重新加載數(shù)據(jù)的開銷。第二個問題是服務(wù)器突然宕機內(nèi)存里有幾萬玩家數(shù)據(jù)還沒落庫你怎么辦這就是經(jīng)典的崩潰恢復問題。我的方案是兩層保障第一層是Redis玩家關(guān)鍵數(shù)據(jù)比如金幣、等級等寫操作同時更新RedisRedis的可靠性由AOF保障宕機后能從Redis恢復大部分數(shù)據(jù)第二層是數(shù)據(jù)庫的binlog如果Redis也丟失了可以通過binlog重放恢復。面試官追問了兩層數(shù)據(jù)不一致怎么辦我回答以數(shù)據(jù)庫為準Redis失敗時重新從DB加載并回填。這個思路是對業(yè)務(wù)后端的數(shù)據(jù)鏈路設(shè)計的一次完整驗證。第三個問題是線上接口突然變慢你怎么排查我按照CPU、內(nèi)存、IO、網(wǎng)絡(luò)四個方向逐一展開。首先用top命令看CPU使用率如果是CPU打滿用jstack抓線程快照分析是GC線程還是業(yè)務(wù)線程導致的如果是內(nèi)存問題用jstat看GC頻率和堆內(nèi)存使用情況必要時增加堆內(nèi)存或者優(yōu)化對象創(chuàng)建如果是IO問題看數(shù)據(jù)庫慢查詢?nèi)罩居袥]有全表掃描或者緩存失效導致的雪崩。面試官對緩存雪崩特別感興趣追問了熱點緩存同時過期怎么辦我給出的方案是過期時間加隨機擾動同時用分布式鎖做緩存重建避免大量請求同時打到數(shù)據(jù)庫。整個項目深挖的節(jié)奏很快但核心邏輯是一致的面試官想看到的是你的代碼出過問題嗎你分析過為什么出問題嗎你怎么避免它再次發(fā)生嗎如果你沒有線上故障處理的真實經(jīng)驗至少要在準備時把簡歷上每個模塊的異常場景都過一遍想想如果遇到極端情況你怎么辦。5. 手撕代碼與場景設(shè)計題5.1 數(shù)據(jù)結(jié)構(gòu)題別只刷LeetCode Hot 100途游的代碼題不是特別偏難怪但和業(yè)務(wù)貼合度很高。第二輪面試的第一個代碼題是給定一個非負整數(shù)數(shù)組和一個目標值找到數(shù)組中是否存在兩個數(shù)的和等于目標值。這道題一看就是Two Sum的變體但我當時沒有直接上HashMap而是問了面試官一個問題數(shù)組是排好序的嗎因為如果是排序數(shù)組可以用雙指針空間復雜度O(1)如果不是再考慮HashMap的O(n)方案。這個先問清楚需求再動手的習慣在游戲后端開發(fā)里很關(guān)鍵因為很多時候產(chǎn)品給的需求是有坑的直接開寫容易返工。面試官對我這個反應是認可的說明見過真實業(yè)務(wù)。第二個代碼題是設(shè)計一個發(fā)牌系統(tǒng)確保每次發(fā)牌的結(jié)果不完全打亂牌堆實際上就是洗牌算法。我寫的是Fisher-Yates洗牌從數(shù)組末尾開始每次隨機選一個位置交換。這個算法的關(guān)鍵是隨機數(shù)的邊界不要寫錯否則會引入偏差。寫完后面試官追加了一個問題如果玩家量很大每秒鐘要發(fā)幾千次牌每次都要打亂一副54張的牌性能瓶頸在哪我回答瓶頸主要在隨機數(shù)生成和數(shù)組拷貝上可以用預生成的隨機數(shù)池以及復用同一個牌數(shù)組對象來避免每次new數(shù)組。這種追問才是游戲后端面試代碼題的真正目的不是看你會不會寫而是看你寫的代碼放到高并發(fā)的游戲環(huán)境里會不會把服務(wù)器拖垮。5.2 場景設(shè)計題排行榜與跨服架構(gòu)第三輪面試有一個大場景題讓我設(shè)計一個跨服排行榜這里我展開講一下完整的答題思路。先明確需求多個服務(wù)器比如20個服的玩家都要參與同一個排行榜榜單實時更新玩家能查看自己的排名和前后50名的積分情況。這個需求卡在跨服上因為不同服的玩家數(shù)據(jù)存儲在不同的分片里如果每秒鐘有上萬次積分變動同步成本會非常高。我的方案分了三層。第一層是在各服內(nèi)部做本地排名即每個服的服務(wù)器在內(nèi)存里維護一份本服玩家的ZSet這個更新是毫秒級的因為不涉及網(wǎng)絡(luò)開銷。第二層是異步同步每秒鐘把本服積分變化超過閾值的前N名玩家數(shù)據(jù)上報到全局排行榜服務(wù)同步用Kafka做異步消息避免直接阻塞本地游戲邏輯。第三層是全局排行榜服務(wù)它維護一個全局ZSet用玩家ID做唯一標識值是一個全局分數(shù)。查排名的時候先查全局ZSet得到大致的名次區(qū)間再結(jié)合本服排名做精確校準。面試官問了一個很現(xiàn)實的問題如果A服上報數(shù)據(jù)延遲了導致全局榜單不準怎么辦我回答榜單本身允許秒級延遲玩家看到的排名是最終一致而不是強一致的這在游戲業(yè)務(wù)里完全可以接受。他又問如果全局Redis掛了怎么辦我的方案是本地榜單不依賴全局服務(wù)玩家玩的還是單服玩法不會中斷全局榜單會記錄最近一次成功同步的時間戳恢復后從該時間點開始補拉數(shù)據(jù)。這個回答把容災設(shè)計也覆蓋了整道題答下來面試官頻頻點頭。5.3 場景設(shè)計題一個吵架系統(tǒng)的反思中途有一個小插曲值得單獨說。有一個場景題是如果玩家在游戲聊天頻道里吵架或者刷屏你怎么設(shè)計系統(tǒng)去管控這個題看似是產(chǎn)品題實際考的是規(guī)則引擎和數(shù)據(jù)流設(shè)計。我的回答用了兩個方案并舉。第一個方案是頻率控制。每個玩家有個發(fā)言計數(shù)窗口比如10秒內(nèi)最多發(fā)3條超過就觸發(fā)禁言或驗證碼。第二個方案是內(nèi)容過濾敏感詞庫用AC自動機做多模式匹配服務(wù)器收到消息后先在內(nèi)存里做一次過濾命中則丟棄并記錄日志。面試官追問道如果玩家用變體字或者拼音繞過過濾怎么辦我回答說內(nèi)容過濾永遠做不到百分百需要疊加人工審核和舉報機制同時要留存聊天日志用于追溯。這道題的用意在于考察你是否能設(shè)計一個低成本、高可用的功能系統(tǒng)而不是真的要做一套完美無缺的AI審核系統(tǒng)。我后來復盤覺得類似的場景題在游戲后端面試里出現(xiàn)頻率極高準備時可以多練習頻率控制內(nèi)容過濾人工兜底這個組合思路。6. 復盤總結(jié)與避坑建議整場面試走完我最大的感受是途游游戲后端面試更看重實踐深度而非廣度。面試官不會拿“你背了多少八股文”來衡量你而是通過項目追問和場景設(shè)計題看你能否在真實的業(yè)務(wù)約束下做技術(shù)決策。以下幾個點是我復盤后覺得最值得分享的。第一個建議是準備面試時要建立自己的面試上下文表。把簡歷里每個項目都整理成項目背景-技術(shù)選型-核心難點-解決問題-效果量化這五段式結(jié)構(gòu)同時為每個難點準備一個如果出問題怎么辦的預案。我這次面試有三個追問都命中了自己提前準備的預案這讓我在面試過程中能比較從容地控制節(jié)奏。第二個建議是代碼題不要只刷Hot 100要結(jié)合游戲場景練手。比如寫一個A*尋路、寫一個環(huán)形隊列、實現(xiàn)一個時間輪定時器、用Redis ZSet做一個排行榜接口。這些題目在游戲后端面試中出現(xiàn)概率極高比盲刷各種偏題要有效得多。我自己在準備時間輪算法的時候本以為用不上結(jié)果面試官問到的定時任務(wù)過期清理方案里正好用到了這個概念。第三個建議是面試中遇到不會的問題千萬不要硬編。游戲后端面試官大多技術(shù)底蘊很厚你說的是不是真實經(jīng)驗幾句話就能感覺出來。我有一道題問的是Kubernetes的Pod調(diào)度策略這塊我確實不熟就如實說這塊我只停留在了解層面沒有實際部署過然后主動把話題引到我熟悉的Docker容器化部署經(jīng)驗上。這種誠實認短板展示長板的方式反而比支支吾吾要好因為面試官要看的是你的學習能力而不是你要成為一個什么都會的百科全書。最后再分享一個比較實用的小技巧面試結(jié)束后一定要在24小時內(nèi)把面試中被問到的問題和你的回答整理成文檔。一方面是為了記錄自己當時的思路方便以后復盤另一方面如果你進入下一輪面試這份文檔能幫你快速回憶這一輪聊了什么面試官之間往往會傳遞反饋下一輪就會在你上一輪的基礎(chǔ)上繼續(xù)深挖如果你銜接不上就會留下這個項目不是你自己做的的印象。如果你也準備投游戲后端方向把上面這些內(nèi)容當成一個參考坐標但不要把它當成標準答案。每個公司的技術(shù)棧和業(yè)務(wù)階段不同面試風格也會不一樣但底層那些東西是通用的并發(fā)編程、網(wǎng)絡(luò)通信、數(shù)據(jù)結(jié)構(gòu)、存儲選型、線上排查這些硬功夫練到位了不管面試官怎么問你都能接得住。