後端工程師的技術面試 Q & A! === 這裡整理一些我看到的或者我本人親自被問到的技術面試題,除了給自己記錄一下也給大家參考。 只寫我記得的,如果有新的會繼續更新。 人性本賤,不被電就不向前。 有事沒事多面試,這是驅動進步最好的方式。 p.s. 我開發的語言比較邪門一點不是目前主流語言而是 Go,但很多概念是差不多的也可以參考。 資料庫相關 === 你說你用過 MySQL,可以講一下事務並發情況會有什麼問題嗎?四種隔離層級又分別對資料讀取寫入有什麼限制? --- 事務並發情況主要會有三個問題:髒讀、不可重複讀、幻讀。 ### 髒讀: 當事務 A 與事務 B 同時操作同一條記錄時出現的問題,假設一條記錄餘額為 100,A 事務要先讀取餘額進行操作,在讀取之前 B 事務對餘額進行扣減了10,此時 B 事務並沒有提交,但 A 讀取到的資料卻是扣減過後的數值,這種情況稱為髒讀。 ### 不可重複讀: 事務 A 要讀取餘額記錄兩次,第一次事務 A 先讀取的餘額數值為 100,此時事務 B 對餘額進行扣減 10 並提交事務,當事務 A 再次讀取餘額時數值就變為 90,與第一次事務讀取到的資料不同,此時稱為不可重複讀。 ### 幻讀: 事務 A 要查詢兩次 id > 10 的資料,第一次查詢時拿到了 11、12 兩筆資料,此時事務 B 開啟並插入了一條 id = 13 的資料並提交。當事務 A 第二次查詢時就出現 11、12、13 三筆資料,此時稱為幻讀。 四種隔離層級就是在解決上面的問題,分別是:讀未提交、讀已提交、可重複讀、串行化。 ### 讀未提交: 隔離級別中最低級別的一種,存在髒讀問題。 ### 讀已提交: 解決了髒讀的問題,但並未解決不可重複讀的問題。 ### 可重複讀: 在這個隔離層級已經解決了不可重複讀及幻讀的問題,但仍然存在一個問題,假設事務 A 查詢兩次 id > 10 的資料,第一次查到 11、12 兩筆資料,事務 B 插入了 id = 13 的資料並提交。此時事務 A 在不知道有 id = 13 的這筆記錄條件下對 id = 13 的資料進行更新,當第二次查詢時就會出現 11、12、13 三筆資料,與第一次查詢不同。MySQL 預設的隔離層級就是可重複讀。 ### 串行化: 最嚴格的一種隔離層級,當發生上述的問題時,事務 B 是不被允許寫入的,而是會等待事務 A 結束並提交後才允許寫入。當然這也造成效能瓶頸。 MySQL 的索引怎麼實現的?可以解釋一下嗎? --- 主要使用 B+tree,這麼做是為了避免資料量大的時候索引效能不一致的問題,儲存資料只在葉子節點,並且每個葉子節點儲存的資料是有序的,葉子節點之間也有索引連接以此來實現範圍查詢時若跨節點不需要再從根節點找到旁邊的節點。 如果你拿到一個查詢語句速度很慢,你優化這個查詢的做法是什麼? --- 除了最基本的在查詢條件用到的欄位加上索引之外還有一些可以注意: 1. 可以查看該 SQL 語句的執行計劃,查看有效資料與總查詢資料的佔比,根據查詢計劃的資料再進行優化。 2. 避免使用 SELECT *,GORM 如果不指定 select 欄位預設都是使用 SELECT * 的,減少查詢的欄位可以加快查詢速度。 3. 使用 where 條件的時候盡量提供精準條件,避免使用 != 或 <> 條件,如果需要可以使用 IN 來避免索引失效。 4. 列上不使用聚合函數,這會讓資料失去有序從而導致索引失效。 5. 當資料量過大時一定要使用 Limit 來限制查詢的資料量,並且在後端實現分頁查詢。 6. 如果查詢仍然很慢,可以考慮分庫分表或讀寫分離,引用快取也可以減輕資料庫的讀寫壓力使整體回應速度更快。 Postgres 和 MySQL 的比較,如果舊專案使用 MySQL 有必要移植到 Postgres 上嗎? --- Postgres 對比 MySQL 在資料靈活度上更自由,支援 jsonb 格式,也就是雖然是關聯式資料庫,但是也可以支持非關聯的特性,雖然後續 MySQL 也更新了 json 格式的支援,但目前他的支援度還是比 Postgres 差一點。論效能上 Postgres 確實比 MySQL 更好一些,如果資料量很大的時候會比 MySQL 好一些,但如果是少量多次的查詢 MySQL 反而會比 Postgres 效能更好。 至於老專案是否要移植資料庫,我認為是沒必要的,MySQL 近幾年也在更新,效能其實與 postgres 的差距不大,如果要儲存一些特殊資料如地理位置等,也可以透過插件來擴充,所以我認為如果要建立一個新專案時,考量是否常常要執行複雜查詢或高度的擴展,如果結構不複雜則完全可以使用 MySQL,老專案的話沒有必要為了一點性能花費大量的時間移植資料。 快取(Redis)相關 === 用過 Redis 嗎?他和我們直接用內存做有什麼不同? --- Redis 本身可以算作是一個基於 RAM 的 NoSQL 資料庫,採用 key-value 結構,對比直接使用內存他支援更多的資料儲存形態,並且 Redis 有支援 RDB 和 AOF 兩種資料持久化存儲的方法,即使系統斷電或出錯,Redis 還是可以很快的取得舊有資料,解決了 RAM 揮發性的問題。 Redis 怎麼保證資料一致性的? --- Redis 本身的特性是單線程,因此每個操作都是具有原子性的,可以保證同一個資料無論讀寫都只有一個操作在進行,也就沒有資料一致性的問題。 如果我們的系統部署在多台主機上,並且用負載均衡器來分散流量,當用戶登入時沒辦法預測流量在哪個主機,如果將登入 session 只存在那台主機中,其他主機會不知道用戶有登入,此時就要不斷重複登入,怎麼解決這個問題? --- 可以透過一個第三方的資料庫來實現 SSO,如 Redis,如果用傳統 session cookie 方法,可以將 session 資訊儲存在 Redis 中,當主機需要確認用戶是否登入時前往 Redis 查看,這樣就解決不同主機讀取不到各自儲存的登入資訊問題。 當然,如果使用 JWT 這個問題就會變得更容易解決,只要所有主機設定的 JWT 秘鑰都相同,就可以驗證用戶 JWT 的有效性,然而缺點就是系統不能強制讓 JWT Token 失效。 在傳統單體架構下可以用鎖來避免資料不一致的問題,但微服務架構下鎖沒辦法鎖到其他服務,怎麼解決這個問題? --- 可以用 Redis 來實現分佈式鎖,利用 Redis 單線程的特性使用 SetNX 方法並指定 key,如果成功寫入資料代表拿到鎖,執行完之後在 Redis 中刪除這筆資料即代表釋放鎖。 如果拿到鎖的線程突然壞掉導致鎖一直不被釋放怎麼辦? --- 在設定鎖的時候同時也要設定資料過期時間,當過期時間到的時候資料會自動刪除,也就代表鎖被釋放,從而解決上述的問題。 那設定了過期時間之後,如果過期時間到了但線程還沒有處理完怎麼辦? --- Redis 有一個配套的套件 redisson,可以使用裡面的 watchdog 機制來檢測當前線程的狀態,如果過期時間到了但尚未執行完,那麼他會自動幫你重新對鎖進行續時,如果線程死機也會幫助釋放該線程持有的鎖。 如果是 Go 語言也有配套的套件 redsync,不過 watchdog 機制就需要自己實現,可以開一個 goroutine 實現 redsync mutex 的監控,並使用 Extend 方法來延長鎖的時間。 Message Queue 相關 === Message queue 有很多,為什麼選擇 RabbitMQ? --- 當初選擇 message queue 時主要考慮三者:kafka、RabbitMQ、NATS。NATS 算是非常新的 message queue,且也使用 Go 開發,在效能及資料丟失率上表現都非常良好,但因為技術較新,相關社群資料較少導致沒有前輩可以討論的我比較難選。 kafka 和 RabbitMQ 之間的選擇就要考慮公司資料量的問題,兩者各有優缺。如果公司數據量非常大,大約千萬級別,那麼我會考慮使用 kafka,在大量數據時 kafka 可以保證資料 0 丟失,如果資料量沒有那麼大我就會選擇 RabbitMQ,RabbitMQ 相對於 kafka 的優勢在於延遲更低,達到微秒級別,比起 kafka 的毫秒級快了不少。 你之前是怎麼保證 RabbitMQ 資料不丟失的? --- RabbitMQ 有一個確認機制,可以等到客戶端主機完成信息消費之後再發送確認信息給 RabbitMQ,當收到確認信息後 RabbitMQ 才會將這筆資料從 queue 中刪除,從而達到資料不丟失。 現在有個遊戲場景,三台遊戲伺服器,某一台伺服器上的玩家抽到了稀有道具要廣播給所有玩家,你會怎麼設計? --- 首先遊戲伺服器會使用 websocket 來與玩家進行通信,這就保證資料可以雙向傳輸,那麼就代表伺服器可以主動推送資料給客戶端。要讓伺服器之間可以彼此通信,那麼可以使用 Message queue 的 Pub/Sub 模型,當有玩家抽到稀有道具時觸發發佈消息至 queue 中,其他伺服器訂閱這個 queue,當收到消息時就可以透過 websocket 主動發送消息給客戶端。 系統架構、網路通信 === 解釋一下你理解的微服務架構 --- 在整個微服務架構中,通常會有一個伺服器負責 api 請求接受,根據請求內容將請求轉發到指定的服務上,服務只執行業務邏輯並回傳結果。 服務之間也可以互相調用,如果要知道各個服務間的網路位置可以使用服務發現工具,將服務註冊上去即可,同時服務發現也會檢查服務的健康狀況,當服務重啟導致內部 ip 位址發生改變時,服務發現也會自動調整,這樣就不需要去每一個服務更改該服務的網路位址。 微服務對比單體架構? --- 微服務中所有服務都是可以單獨部署的,在對系統進行更新時可以只更新某個服務,不會導致整個系統都無法使用,如果某個服務壞掉也不會導致整個系統崩潰,另外各個服務之間也不要求要使用同一語言編寫,但對比單體架構就增加了不少運維成本,也因此衍生出 kubernetes 這樣的工具。 如果兩者要選的話,要看公司整體的系統功能複雜度,如果功能很多很雜且流量較大,或者公司內後端工程師熟悉的語言並不相同,那麼使用微服務架構會更好。如果專案大小較小,需管理的功能模塊不是很多,那麼使用單體架構會是更輕鬆的選擇。 用過 gRPC 嗎?跟 http 怎麼選? --- 先確認一下這裡的 http 說的是 http 1.1,兩者各有優缺,http 是老協議了,基本所有的套件都一定最先支援 http 協議,在開發選擇上比起 gRPC 會有更多選擇可以使用,另外在跨語言調用中 http 也基本不會出問題。 gRPC 作為基於 http 2.0 的協議支援表頭壓縮及 streaming,而且對於 protobuf 的資料傳輸格式有更好的支援,在整體傳輸速度上比 http 上快了不少,但相對於開發上比起 http 就會更複雜,而且可能會遇到套件不支援的問題,例如在目前的時間,go-micro 這個微服務框架正在轉往 v5 版本,不過 gRPC 的支援只到 v4,這就造成開發上的一些問題。