今天分享一下訓練營內部朋友在B站遊戲服務器開發面試的詳解,
主要整理了問到的技術問題,項目介紹類問題去掉了,覆蓋分佈式、中間件、數據庫、併發控制等知識點,大家可以參考學習一下。
一面
1. 項目最終一致性的設計思路
核心思路:基於“事務消息+重試機制+冪等性”實現,優先選擇低侵入性方案,適用於訂單支付後庫存、積分、日誌等跨服務同步場景。
具體實現(以訂單支付為例):
- 本地事務與消息發送原子性:使用“本地消息表+定時任務”或 RocketMQ 事務消息。比如用 RocketMQ 時,先執行本地訂單更新(狀態改為“待支付”→“已支付”),成功後提交事務消息,失敗則回滾本地事務。
- 消息消費與重試:下游服務(庫存、積分)訂閲事務消息,消費成功則更新自身狀態,失敗則觸發 MQ 重試(階梯式重試:10s/30s/5min,避免瞬時故障)。
- 冪等性保障:每個消息攜帶唯一 ID(如訂單號+流水號),下游服務消費前先查“消息消費記錄表”,已消費則直接返回成功,未消費則執行邏輯。
- 最終兜底:定時任務掃描“未同步成功”的訂單,主動觸發補償邏輯(如調用庫存服務接口重試),確保最終所有服務狀態一致。
2. 項目異步設計的思路
核心思路:解耦服務依賴、提高吞吐量,優先用“消息隊列+Go 協程”組合,覆蓋跨服務異步和本地異步場景。
具體設計:
- 跨服務異步(解耦):用 Kafka/RocketMQ 做異步通信,比如用户註冊後,同步發送“註冊成功”消息,下游服務(短信、郵件、日誌)訂閲消費,主流程無需等待。
- 本地異步(提效):用 Go 協程處理無依賴的本地任務,比如訂單創建後,啓動協程異步生成訂單快照、記錄操作日誌,通過
sync.WaitGroup控制協程等待(如需等待結果)或channel傳遞結果。 -
關鍵保障:
- 冪等性:同最終一致性的消息 ID 校驗;
- 超時處理:用
context.WithTimeout控制協程執行時間,避免阻塞; - 錯誤處理:協程 panic 捕獲(
defer recover())、消息消費失敗入死信隊列,定期覆盤; - 結果回調:如需同步異步結果,用“回調函數+channel”或“狀態輪詢”(如前端輪詢訂單支付狀態)。
項目落地:遊戲充值接口通過異步化改造,吞吐量從 500 QPS 提升至 3000 QPS,響應時間從 300ms 降至 50ms。
3. 消息隊列怎麼消費不同標籤的信息
以 RocketMQ(Tag 機制)和 Kafka(Topic+Partition 二級分類)為例,核心是“ broker 端過濾+消費端訂閲”:
- 標籤(Tag)設計:Tag 是消息的二級分類,基於業務場景劃分(如訂單消息:
ORDER_PAID/ORDER_CANCELLED/ORDER_REFUNDED)。 -
消費端訂閲邏輯(Go 實現):
- RocketMQ:使用 Go SDK(如
github.com/apache/rocketmq-client-go),在創建消費者時,通過ConsumerOption指定訂閲的 Tag,格式為Topic:Tag1||Tag2(多 Tag 用||分隔),Broker 會僅將匹配 Tag 的消息投遞給消費者。 - Kafka:無原生 Tag,但可通過“Topic+消息頭”模擬,消費端讀取消息頭中的
tag字段過濾,或直接按 Tag 拆分 Topic(如order_paid_topic/order_cancelled_topic),更高效。
- RocketMQ:使用 Go SDK(如
- 優勢:Broker 端過濾減少無效消息傳輸,提升消費效率;消費端可靈活訂閲所需 Tag,實現業務解耦。
4. Golang 的線程池、協程池的使用?比如 running buffer
Go 無內置線程池/協程池,但協程(Goroutine)輕量(初始棧 2KB),可通過 channel 手動實現協程池,核心是“控制併發數+任務調度”:
(1)協程池核心設計
- 核心組件:任務隊列(
taskChan)、worker 協程池、併發控制(maxWorkers)、運行狀態標識(running buffer,即當前活躍 worker 數)。 -
實現步驟(Go 代碼簡化):
type Task func() error type Pool struct { taskChan chan Task // 任務隊列 maxWorkers int // 最大併發數 running int32 // 當前運行的worker數(原子變量,避免競態) ctx context.Context cancel context.CancelFunc } // 初始化協程池 func NewPool(maxWorkers int) *Pool { ctx, cancel := context.WithCancel(context.Background()) pool := &Pool{ taskChan: make(chan Task, 100), // 任務隊列緩衝 maxWorkers: maxWorkers, ctx: ctx, cancel: cancel, } // 啓動worker for i := 0; i < maxWorkers; i++ { go pool.worker() } return pool } // worker協程:循環消費任務 func (p *Pool) worker() { defer atomic.AddInt32(&p.running, -1) atomic.AddInt32(&p.running, 1) for { select { case <-p.ctx.Done(): return case task, ok := <-p.taskChan: if !ok { return } _ = task() // 執行任務 } } } // 提交任務 func (p *Pool) Submit(task Task) error { select { case <-p.ctx.Done(): return fmt.Errorf("pool closed") case p.taskChan <- task: return nil } }
(2)關鍵概念與使用場景
running buffer:用atomic.Int32維護當前運行的 worker 數,可用於監控協程池負載(如通過 Prometheus 暴露指標)。- 使用場景:高併發 I/O 操作(如批量調用第三方接口、數據庫批量寫入)、避免無限制創建協程導致的內存溢出。
- 注意點:任務隊列需設置緩衝(避免提交任務阻塞)、worker 優雅退出(通過 context 控制)、錯誤處理(任務執行失敗需記錄日誌或重試)。
5. 用的什麼中間件監聽數據庫 binlog
項目中用 Canal 監聽 MySQL binlog,核心是“模擬 MySQL 從庫同步協議,解析 binlog 並推送變更”:
-
工作流程:
- Canal 偽裝成 MySQL 從庫,向主庫發送 dump 命令,獲取 binlog 日誌;
- 解析 binlog(支持 row 格式,記錄具體數據變更),提取表名、操作類型(insert/update/delete)、變更前後數據;
- 通過 Canal Client(Go SDK:
github.com/alibaba/canal-go)訂閲變更事件,推送至業務邏輯(如同步數據到 Redis、ES,或觸發跨服務通知)。
- 優勢:輕量、低侵入(無需修改業務代碼)、支持高可用部署(Canal Server 集羣)。
6. Redis 常用的數據結構
| 數據結構 | 核心用途 | 項目應用場景 |
|---|---|---|
| String | 簡單鍵值存儲、計數器 | 存儲玩家驗證碼(key=player:{id}:code)、遊戲在線人數計數(INCR/DECR) |
| Hash | 複雜對象存儲(字段-值映射) | 存儲玩家信息(key=player:{id},field=name/level/gold)、商品屬性 |
| List | 隊列、棧、消息列表 | 遊戲公告隊列(LPUSH/RPOP)、玩家郵件列表 |
| Set | 去重、交集/並集運算 | 玩家好友關係(SADD/SISMEMBER)、抽獎活動去重(避免重複中獎) |
| Sorted Set | 有序排序、排行榜 | 遊戲戰力排行榜(ZADD/ZRANGE)、限時活動積分排名 |
| Bitmap | 位運算、布爾值存儲 | 玩家簽到記錄(key=sign:{date},bit=playerID,1=已簽到) |
| Geo | 地理位置計算 | 遊戲附近玩家查找(GEORADIUS) |
進階用法:Hash 用 HSCAN 避免大 key 阻塞、Sorted Set 用 ZREMRANGEBYRANK 維護TopN排行榜、String 用 SETEX 實現過期緩存。
7. ETCD 的作用
ETCD 是分佈式鍵值存儲(基於 Raft 協議),核心作用是“分佈式一致性保障”,項目中主要用於 3 個場景:
- 服務註冊與發現:微服務(如遊戲網關、戰鬥服、道具服)啓動時向 ETCD 註冊(key=/services/{serviceName}/{instanceID},value=服務地址+元數據),客户端通過 ETCD 的 Watch 機制監聽服務變更,動態獲取可用實例(配合 gRPC 負載均衡)。
- 配置中心:存儲全局配置(如數據庫連接池大小、活動開關、限流閾值),通過 Watch 機制實現配置動態更新(無需重啓服務),Go 中用
etcd/clientv3訂閲配置變更。 - 分佈式鎖:基於 ETCD 的 Lease(租約)+ CAS 操作實現,用於跨服務併發控制(如遊戲跨服活動報名、分佈式任務調度),避免死鎖(租約過期自動釋放鎖)。
優勢:強一致性、高可用(集羣部署)、輕量、支持 TTL 過期鍵。
8. 百庫百表分庫分表思路(玩家場景)
核心思路:水平分片(按玩家 ID 哈希分片),目標是分散數據壓力、提升查詢效率,適配百萬級玩家數據存儲:
- 分片維度選擇:按玩家 ID 分片(玩家操作自身數據時,可直接路由到對應庫表,無跨庫聯查)。
-
分片策略:
- 分庫分表規則:100 庫 × 100 表 = 10000 張表。玩家 ID 經過哈希計算(如
hash(playerID) % 100)得到庫索引,hash(playerID) / 100 % 100得到表索引,最終路由到db{庫索引}.t_player_{表索引}。 - 哈希算法:用一致性哈希(帶虛擬節點),支持後續擴容(新增庫表時僅遷移部分數據,影響範圍小)。
- 分庫分表規則:100 庫 × 100 表 = 10000 張表。玩家 ID 經過哈希計算(如
- 中間件選型:ShardingSphere-JDBC(Go 項目中用
shardingsphere-go),透明化分庫分表邏輯(業務代碼無需關注分片規則,直接操作邏輯表)。 -
關鍵問題解決:
- 全局 ID:用雪花算法(Snowflake)生成唯一訂單號/玩家 ID,避免分庫分表後 ID 衝突。
- 跨庫查詢:避免跨庫聯查,通過“寬表冗餘”(如玩家訂單表冗餘玩家基礎信息)或“應用層聚合”(先查各庫數據,再在服務端合併)。
- 擴容方案:新增庫表時,基於一致性哈希遷移舊數據,雙寫新舊庫一段時間(確保數據一致),再切換到新庫表。
項目落地:遊戲玩家中心存儲 500 萬玩家數據,分 100 庫 100 表,單表數據量控制在 5000 以內,查詢響應時間穩定在 10ms 內。
二面
1. 壓測時遇到的性能瓶頸及解決
壓測工具:用 k6(Go 編寫,高併發支持)+ Prometheus+Grafana 監控指標(QPS、響應時間、CPU/內存/網絡),遇到的核心瓶頸及解決方案:
| 瓶頸類型 | 現象 | 排查方式 | 解決方案 |
|---|---|---|---|
| 數據庫慢查詢 | 接口響應時間>500ms,MySQL CPU 100% | EXPLAIN 分析 SQL,慢查詢日誌 | 1. 給訂單表添加聯合索引(player_id+create_time);2. 分頁查詢優化(用遊標代替 limit offset);3. 讀寫分離(讀請求路由到從庫) |
| Redis 緩存穿透 | 大量請求穿透到數據庫,Redis 命中率<80% | Redis 監控面板查看命中率 | 1. 無效 key 緩存空值(SETEX key 3600 "");2. 布隆過濾器(RedisBloom)過濾不存在的玩家 ID |
| 協程泄露 | 內存持續增長,協程數>10w | pprof 分析 goroutine 棧 | 1. 協程池控制併發數(maxWorkers=100);2. 用 context.WithTimeout 控制協程生命週期,避免無限阻塞 |
| 網絡瓶頸 | 跨服務調用延遲>200ms | tcpdump 抓包,鏈路追蹤(Jaeger) | 1. 服務本地緩存熱點數據(如活動配置);2. gRPC 連接池優化(複用連接,減少握手開銷) |
優化結果:接口 QPS 從 800 提升至 5000,響應時間穩定在 50-80ms,CPU 使用率控制在 70% 以內。
2. MySQL 相關優化
從“索引、SQL、配置、架構”四層優化,結合項目實踐:
-
索引優化:
- 核心原則:給查詢頻繁的字段建索引,避免過度索引(影響寫入性能);
- 實踐:玩家訂單表(player_id、create_time、status)建聯合索引,覆蓋查詢(
select id, amount from t_order where player_id=? and status=? order by create_time desc),避免回表。
-
SQL 優化:
- 避免 select *(只查需要的字段)、避免
or(用 union 代替)、子查詢轉 join; - 實踐:將“查詢玩家近 30 天訂單並關聯商品信息”的子查詢,改為 join 查詢,執行時間從 300ms 降至 50ms。
- 避免 select *(只查需要的字段)、避免
-
配置優化:
- innodb_buffer_pool_size = 物理內存的 50%-70%(緩存數據和索引,減少磁盤 I/O);
- max_connections = 2000(適配高併發場景);
- 關閉 binlog 或設置為 row 格式(減少 binlog 體積,提高寫入性能)。
-
架構優化:
- 主從複製(一主兩從),讀請求分流到從庫(通過 ShardingSphere-JDBC 實現讀寫分離);
- 分庫分表(如玩家表、訂單表),分散單庫單表壓力。
3. 實際項目中發現 MySQL 查詢瓶頸的方法
核心是“監控+日誌+執行計劃”三位一體,步驟如下:
- 慢查詢日誌定位:開啓 MySQL 慢查詢日誌(
slow_query_log=1,long_query_time=1),捕獲執行時間>1s 的 SQL,定期分析日誌(用 pt-query-digest 工具彙總)。 - 執行計劃分析:對慢查詢用
EXPLAIN分析,重點看type(索引類型,如 ref、range 優於 all)、key(是否使用索引)、rows(掃描行數,越少越好)、Extra(是否 Using filesort/Using temporary,需優化)。 - 實時監控:通過 Prometheus+Grafana 監控 MySQL 指標(
slow_queries慢查詢數、innodb_rows_read掃描行數、Threads_running運行線程數),設置閾值告警(如慢查詢數>10 觸發告警)。 - 業務日誌關聯:在應用日誌中記錄 SQL 執行時間(如 Go 中用
sqlx攔截器),當接口響應變慢時,直接定位到耗時 SQL。
項目案例:通過慢查詢日誌發現“玩家累計充值金額查詢”SQL 未走索引,掃描全表(rows=50w),用 EXPLAIN 分析後,給 player_id 建索引,查詢時間從 1.2s 降至 8ms。
4. 分佈式系統 100 台服務器,玩家報錯的處理流程
核心思路:快速定位故障範圍→精準排查根因→臨時止損→永久修復,步驟如下:
- 收集報錯信息:讓玩家提供“報錯提示(如‘支付失敗’)、操作時間、玩家 ID、服務器區服”,前端同時上報報錯時的 traceID(鏈路追蹤 ID)。
-
定位故障服務與節點:
- 通過 traceID 在 Jaeger 中查詢跨服務調用鏈路,確認是哪個服務(如支付服、訂單服)報錯;
- 在 ELK 日誌平台中,按“traceID+玩家 ID+時間範圍”過濾日誌,找到報錯的服務器節點(IP+端口)。
-
排查節點問題:
- 應用日誌:查看該節點的錯誤堆棧,定位代碼層面問題(如空指針、數據庫連接超時);
- 系統監控:查看節點的 CPU、內存、磁盤 I/O、網絡(用 Prometheus+Grafana),是否存在資源耗盡;
- 依賴服務:檢查該節點依賴的數據庫、Redis、MQ 是否正常(如 Redis 連接超時、數據庫主從切換)。
-
臨時止損:
- 若單節點故障:通過負載均衡下線該節點,將流量轉發到其他健康節點;
- 若服務級故障:觸發熔斷(如用 Hystrix/Resilience4j),返回友好提示(“系統臨時維護,請稍後再試”),避免雪崩。
-
永久修復與覆盤:
- 修復代碼 bug(如空指針判斷、重試機制優化);
- 優化監控告警(補充關鍵鏈路告警);
- 覆盤會議,總結故障原因(如“未處理 Redis 連接超時”),避免同類問題。
5. 如何定位日誌
基於“分佈式日誌架構+鏈路追蹤”,實現日誌快速定位,架構:ELK(Elasticsearch+Logstash+Kibana)+ 鏈路追蹤(Jaeger):
-
日誌規範:
- 統一日誌格式(JSON 格式),包含核心字段:
traceID(鏈路追蹤 ID)、spanID、serviceName(服務名)、instanceIP(節點 IP)、playerID(玩家 ID)、time(時間戳)、level(日誌級別)、msg(日誌內容)、stack(錯誤堆棧)。 - 鏈路追蹤透傳:用 gRPC 攔截器或 HTTP 中間件,在服務間傳遞
traceID,確保同一請求的所有日誌都攜帶相同traceID。
- 統一日誌格式(JSON 格式),包含核心字段:
-
定位步驟:
- 玩家報錯後,獲取
traceID(從前端或玩家提供的報錯信息中提取); - 打開 Kibana,在索引中按
traceID:xxx過濾,獲取該請求的所有日誌(從網關→業務服→依賴服務); - 按時間排序日誌,找到報錯節點的錯誤堆棧,定位問題(如“支付服調用微信支付接口超時”);
- 結合 Jaeger 查看該
traceID的調用鏈路,確認超時環節(如微信支付接口響應時間>3s)。
- 玩家報錯後,獲取
項目落地:通過該方案,將日誌定位時間從 30 分鐘縮短至 5 分鐘,大幅提升故障排查效率。
6. 超買超賣的訂單處理
核心是“併發控制+原子操作”,基於 Redis+MySQL 實現雙重保障:
-
方案一:Redis 分佈式鎖+庫存預扣減(高併發場景首選)
- 鎖 key:
lock:goods:{goodsID}(同一商品共享一把鎖); -
流程:
- 玩家下單時,用 Redis SET NX EX 命令獲取鎖(
SET lock:goods:123 1 EX 10 NX); - 獲取鎖成功後,查詢 Redis 庫存(
GET goods:stock:123),庫存不足則返回“商品已售罄”; - 庫存充足則預扣減(
DECR goods:stock:123),釋放鎖(DEL lock:goods:123); - 預扣減成功後,異步寫入數據庫(訂單表+庫存表),數據庫庫存表加行鎖(
select stock from t_goods where id=? for update),確保最終庫存一致。
- 玩家下單時,用 Redis SET NX EX 命令獲取鎖(
- 注意:鎖超時時間需大於業務執行時間,避免死鎖;用 Lua 腳本保證“查庫存+扣庫存”原子性。
- 鎖 key:
-
方案二:MySQL 樂觀鎖(低併發場景,無鎖競爭)
- 庫存表添加
version字段; - 扣庫存 SQL:
update t_goods set stock=stock-1, version=version+1 where id=? and stock>=1 and version=?; - 執行後判斷影響行數,若為 0 則説明庫存不足或已被其他請求扣減,返回“操作失敗”。
- 庫存表添加
項目落地:遊戲限時搶購活動用方案一,支持 1w+ QPS 併發下單,超買超賣率為 0,庫存一致性 100%。
7. 併發場景避免二次執行(如重複發貨)
核心是“冪等性設計”,結合業務場景選擇以下方案:
-
方案一:唯一請求 ID(客户端層面)
- 客户端(如遊戲客户端)生成唯一請求 ID(UUID),每次請求攜帶該 ID;
- 服務端接收請求後,先查 Redis:
EXISTS request:id:{requestID},存在則返回“操作已執行”,不存在則執行業務邏輯; - 業務邏輯執行成功後,將請求 ID 存入 Redis(
SET request:id:{requestID} 1 EX 3600),過期時間設為業務操作有效時間。
-
方案二:業務唯一鍵(數據庫層面)
- 訂單表創建唯一索引:
UNIQUE KEY uk_player_goods (player_id, goods_id, activity_id)(同一玩家同一活動同一商品只能創建一次訂單); - 重複請求時,數據庫會拋出
Duplicate key error,服務端捕獲後返回“操作已執行”。
- 訂單表創建唯一索引:
-
方案三:分佈式鎖(服務端層面)
- 鎖 key:
lock:player:{playerID}:goods:{goodsID}(同一玩家同一商品的操作共享一把鎖); - 只有獲取鎖的請求能執行業務邏輯,其他請求等待或直接返回,避免併發執行。
- 鎖 key:
項目落地:遊戲道具發放用“方案一+方案二”,既通過請求 ID 快速攔截重複請求,又通過數據庫唯一索引兜底,確保無二次發貨。
8. 支付體系的回調
支付回調是支付平台(微信/支付寶)向商户服務器發送的異步支付結果通知,核心流程:“簽名驗證+冪等處理+訂單更新+響應確認”:
-
完整流程:
- 回調配置:在支付平台(如微信支付商户平台)配置回調地址(必須 HTTPS,公網可訪問);
- 回調觸發:用户支付成功後,支付平台向回調地址發送 POST 請求,參數包含:支付流水號、訂單號、支付金額、簽名等;
- 簽名驗證:服務端用商户密鑰驗證參數簽名(如微信支付的 HMAC-SHA256 簽名),確保請求來自官方,防止偽造;
- 冪等處理:通過訂單號查詢本地訂單狀態,若已處理(如“已支付”),直接返回成功響應;
- 業務處理:未處理則更新訂單狀態為“已支付”,執行後續邏輯(扣庫存、發道具、加積分);
- 響應確認:向支付平台返回指定格式的成功響應(如微信支付返回
<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>),否則支付平台會階梯式重試(如 15s/30s/1min/2min/5min/10min/30min/1h/2h/6h/15h,共 11 次)。
-
關鍵注意點:
- 簽名驗證:必須驗證,避免惡意回調;
- 冪等處理:支付平台會重試,必須保證回調處理冪等;
- 日誌記錄:詳細記錄回調參數、處理結果,便於排查問題;
- 超時處理:回調處理時間需<10s,避免支付平台判定超時重試。
9. 僅用 Redis 和 MySQL 防止多個請求反覆執行
核心是“Redis 原子操作快速攔截+MySQL 唯一索引兜底”,無需額外中間件:
-
實現方案(以玩家購買商品為例):
-
步驟 1:Redis 原子判斷+鎖定(快速攔截)
- 玩家發起購買請求時,服務端執行 Redis 命令:
SETNX lock:player:{playerID}:goods:{goodsID} 1 EX 30(30s 過期,避免死鎖); - 若返回 1(獲取鎖成功),則繼續執行;若返回 0(已被其他請求鎖定),則返回“操作中,請稍後再試”。
- 玩家發起購買請求時,服務端執行 Redis 命令:
-
步驟 2:MySQL 唯一索引兜底(防止 Redis 宕機)
- 訂單表創建唯一索引:
uk_player_goods (player_id, goods_id),確保同一玩家同一商品只能創建一次訂單; - 執行訂單插入 SQL:
insert into t_order (player_id, goods_id, amount, status) values (?, ?, ?, ?); - 若插入成功(影響行數=1),則執行後續邏輯(扣庫存、發道具);若拋出
Duplicate key error,則返回“操作已執行”。
- 訂單表創建唯一索引:
-
步驟 3:釋放鎖
- 訂單創建成功或失敗後,執行
DEL lock:player:{playerID}:goods:{goodsID}釋放鎖(或等待自動過期)。
- 訂單創建成功或失敗後,執行
-
-
優勢:
- Redis 層面快速攔截高併發重複請求,減少數據庫壓力;
- MySQL 唯一索引兜底,即使 Redis 宕機,也能防止重複執行;
- 無額外中間件依賴,部署簡單。
項目落地:遊戲內玩家購買月卡場景用該方案,支持 5k+ QPS 併發請求,無重複購買、重複發貨問題。
歡迎關注 ❤
我們搞了很多免費的面試真題共享羣,互通有無,一起刷題進步。
沒準能讓你能刷到自己意向公司的最新面試題呢。
感興趣的朋友們可以加我微信:wangzhongyang1993,備註:面試羣。