【大廠面試真題解析】蝦皮 Shopee 後端一面十四問

語言: CN / TW / HK

關於我:微信公眾號:面試官問,原創高質量面試題,始於面試題,但不止於面試題。【大廠面試真題解析】面試系列文章將會對大家實際面試中遇到的面試題進行彙總分析,以饗讀者。

本文是作者 gopherhiro [1] 近期面試 Shopee 被問到的問題,本文節選了其中通用的部分進行了解析,歡迎讀者將面試中遇到的問題私信我,可以幫大家整理出回答的要點。

MySQL

1. 為什麼要分庫分表

單個數據庫例項能夠承載的併發訪問量和資料量是有限的,當系統的併發訪問量或者資料量超過這個限制時,就需要考慮使用分庫分表來優化系統的架構設計,使其能夠承載更大的併發量和資料量,並給使用者提供良好的使用體驗。當然,分庫和分表其實是兩個事情,兩者並不是都會同時出現。

2. 分庫解決了什麼問題

分庫主要解決的是 併發量大 的問題,因為單個數據庫例項能夠提供的連線數是有限的,當系統併發量不斷增加,我們勢必需要增加更多的微服務例項來承載更多的業務請求,而每個微服務例項都會佔用一定量的資料庫連線,因此,當資料庫連線數不夠用了,就只能通過增加資料庫例項的方式來提供更多的資料庫連線,進而提升系統整體的併發度。

3. 分表解決了什麼問題

分表主要解決的是 資料量大 的問題,因為單表的資料量很大時,即使併發訪問量不大,但單表的儲存和查詢的效能已經遭遇了瓶頸,通過索引優化等手段雖然能夠一定程度上提升效率,但當單表資料量超過 500 萬行或者單表儲存容量超過 2GB(經驗值,實際要看業務的具體情況) 之後,分表就應該提上日程了。一般都是將資料拆分到多張表中,來減少單表的資料量,從而提升查詢的速度。

4. 樂觀鎖與悲觀鎖?在實踐中是否用過,可否舉例說明一下。

樂觀鎖實際上並沒有加鎖,只是一種鎖思想,一般通過在行資料上新增 版本號欄位 實現,在更新資料前,先查詢出當前行資料的版本號,更新資料時,將版本號加1,並判斷資料庫中版本號是否等於前面我們讀取出來的版本號,如果不一致,說明資料被修改過,更新失敗。樂觀鎖的核心語句是: update table set ... version=version+1 where id=#{id} and version=#{version};

樂觀鎖使用場景:比較適合讀多寫少的場景,因為如果出現大量的寫入操作,version欄位值發生衝突的可能性就會增大,更新失敗後,應用層需要不斷的重新獲取最新version欄位資料,並重試操作,這樣會增加大量的查詢操作,降低了系統的吞吐量。

悲觀鎖一般是使用 select ... for update; 語句鎖定行資料,更新完提交事務後自動釋放行資料,在此期間,其他事務無法更新這一行資料。

悲觀鎖使用場景:比較適合寫多讀少的場景,因為如果出現大量的讀取操作,每次讀的時候都進行加鎖,會增加大量的鎖的開銷,降低了系統的吞吐量。

5. 主鍵索引和唯一索引的區別?

MySQL 資料庫索引按照功能進行劃分,可以分為:

普通索引:沒有任何的約束作用,它存在的主要意義就是提高查詢效率。 唯一索引:在普通索引的基礎上,增加了唯一性約束,要求索引列的值必須唯一,但可以為空值,一張表中可以同時存在多個唯一性索引 主鍵索引:在唯一性索引的基礎上又增加了不為空的約束,而且一張表裡最多隻能有一個主鍵索引,但一個主鍵索引中可以包含多個欄位。 全文索引:實際上用的不多,不做介紹

Redis

1. Redis 崩潰時,如何保證資料不丟失?

Redis 是一個記憶體鍵值對資料庫,如果伺服器程序掛掉,記憶體中的資料就會丟失,為了避免資料丟失,Redis 提供了三種持久化方案:

RDB 持久化:Redis DataBase,將記憶體中的資料以快照(二進位制)的形式儲存到磁碟上,是 Redis 預設的持久化方式。執行完 RDB 持久操作後,會在指定的目錄中生成一個  dump.rdb 檔案,在 Redis 重啟時,會載入  dump.rdb 檔案來恢復資料到記憶體中。RDB 持久化可以通過手動和自動兩種方式觸發:

手動方式:同步方式 save,會阻塞 Redis 主執行緒;非同步方式 bgsave,會 fork 一個子程序,由子程序負責 RDB 檔案的操作,避免阻塞 Redis 服務主程序 自動方式:save m n,當 m 秒內資料集發生 n 次修改時,自動觸發 bgsave

AOF 持久化:Append Only File,基於日誌來記錄 Redis 的每個寫操作,每個操作會追加到檔案的末尾。Redis 預設不開啟 AOF。需要注意的是,AOF 是在執行完 Redis 命令才記錄日誌的,而不是執行之前,因為 Redis 是不會對輸入的命令進行語法檢查的,因此,只有真正執行完命令後,才能避免將非法的命令寫入 AOF 檔案中。AOF 持久化方案有三種日誌寫回策略  appendfsync

always:同步執行日誌寫回,也就是在每個命令執行完之後,立即將日誌寫入 AOF 檔案末尾 everysec:每隔一秒將 AOF 記憶體緩衝區中的日誌重新整理到磁碟中 no:Redis 只負責將日誌寫入到 AOF 記憶體緩衝區中,由作業系統的刷盤機制決定什麼時候寫入磁碟

混合持久化:RDB 方式的優點是檔案相比 AOF 小,資料恢復快,適合大規模資料恢復場景,例如資料備份等;AOF 的優點是資料一致性和完整性相比 RDB 高,通常使用 everysec 寫回策略保證只有秒級的資料丟失。為了中和兩者的優缺點,Redis 4.0 引入了混合持久化,也就是在兩次 RDB 持久化中間,會增加 AOF 操作來記錄這段時間的日誌。

2. Redis 基本資料型別及其使用場景有哪些?

Redis 有五種常用的基本資料型別:

字串 String:不同微服務例項之間 session 共享,分散式鎖、ID生成器、計數器、限速器等 列表 List:實現訊息佇列功能,快取文章列表資訊等 雜湊 Hash:快取使用者資訊 UserInfo(可能包含 userId、userName、password、email 等欄位)、實現短網址生成程式 集合 Set:儲存使用者的標籤資訊、唯一計數器、點贊等功能 有序集合 ZSet:實現排行榜、時間線等功能

3. Redis zset 資料型別底層是如何實現的?

Redis zset 底層資料結構有兩種選型:壓縮列表 ziplist 和跳錶 skiplist,具體選擇哪種資料結構要看當前儲存的資料量和資料大小。當滿足如下兩個條件時,Redis 選擇 ziplist 來實現 zset 值的儲存:

所有資料的大小都要小於 64 位元組 元素個數小於 128 個

當不滿足以上兩個條件時,則會選擇使用 skiplist 來實現 zset 值的儲存。

壓縮列表是 Redis 自己設計的一種資料儲存結構。它有點兒類似陣列,通過一片連續的記憶體空間,來儲存資料。不過,它跟陣列不同的一點是,它允許儲存的資料大小不同。

關於壓縮列表更詳細介紹可以參考極客時間的這篇文章: 52 | 演算法實戰(一):剖析Redis常用資料型別對應的資料結構 [2]

跳錶 skiplist 是在單鏈表基礎上增加了多級索引實現的一個數據結構,能夠實現 O(logn) 時間複雜度的查詢、插入和刪除操作。

關於跳錶更詳細介紹可以參考極客時間的這篇文章: 17 | 跳錶:為什麼Redis一定要用跳錶來實現有序集合? [3]

4. Redis 分散式鎖是如何實現的?

通過呼叫 Redis 命令 SETNX+EXPIRE 實現,同時為了保證原子性,可以通過 lua 指令碼來實現鎖的設定和過期時間設定的原子性。在 Redis 2.6.12 版本後,SETNX 命令增加了過期時間引數,也可以直接使用這個過載方法,SETNX 返回 1 表示獲得 key 所代表的鎖,返回 0 表示獲取鎖失敗。

更多關於分散式鎖的問題,可以參考 場景化面試:關於分散式鎖的十問十答 這篇文章。

5. Redis分散式鎖過期了但業務還沒有執行完,怎麼辦?

這種情況可以通過鎖續約機制來解決,也就是通過另外一個執行緒使用心跳機制來不斷延長鎖的超時時間。

業務監控相關

1. 所做業務介面效能耗時是多少?

按你的系統實際回答即可,針對一般 OLTP 系統來說,介面耗時應該小於 500ms,對於高頻介面,應該保證在 200ms 以內,具體還是要針對具體業務進行分析。

2. 所做業務 QPS 大致是多少?

這個主要是瞭解你的專案的併發情況,看你是否有高併發的相關經驗,即使你的專案本身 QPS 不高,也應該準備下高併發相關的知識以便應對。

3. 如何理解 p 分位?如 p99,p95。

響應時間是指從前端發出請求開始到最後收到響應所需要的時間,對於網際網路服務來說,響應時間我們更應該關注 分位線 ,也就是常說的 TP95、TP99 或 95 線、99 線。

對於 TP95而言,就是將對應介面所有請求的響應時間從小到大排序,位於 95% 這個位置的請求的響應時間,它表示至少有 95% 的請求響應時間小於等於這個值。

系統設計

1. 如何設計一個分散式 ID 生成器?

分散式 ID 必須保證全域性唯一性,常見的方案有 UUID 和雪花演算法(Snowflake)兩種方案,但 UUID 相比雪花演算法存在如下缺點,所以一般來說,我們都會選擇雪花演算法,並根據具體的業務場景進行改造:

UUID 生成的 id 是無序的,而 Snowflake 生成的 id 是有序的,id 有序能夠支援排序,也能夠提升資料的寫入效能 UUID 不具備業務含義,但 Snowflake 具備業務含義,Snowflake 的核心思想是將 64bit 的二進位制數字分成若干部分,每一部分都儲存有特定含義的資料,標準的 Snowflake 演算法包含 1 位符號位、41 位時間戳、10 位機器 id、12 位序列號,最終拼接生成全域性唯一的有序 id。

通常來說,對 Snowflake 的改造是保持前面 42 位生成方式不變,調整後面的 22 位位元位,在其中加入業務相關的資訊。

分散式 ID 生成器的演算法確定後,我們可以將其作為一個 jar 包提供給業務方使用,或者也可以將其獨立封裝成一個基礎服務對外提供 API。具體可以根據自己專案的實際情況確定。

關於分散式 ID 生成器更詳細介紹可以參考極客時間的這篇文章: 10 | 發號器:如何保證分庫分表後ID的全域性唯一性? [4]

References

[1] gopherhiro:  http://leetcode.cn/u/gopherhiro/

[2] 52 | 演算法實戰(一):剖析Redis常用資料型別對應的資料結構:  http://gk.link/a/11xJc

[3] 17 | 跳錶:為什麼Redis一定要用跳錶來實現有序集合?:  http://gk.link/a/11xJd

[4] 10 | 發號器:如何保證分庫分表後ID的全域性唯一性?:  http://gk.link/a/11xJA