1. 程式人生 > >【真實面試經歷】我和阿里面試官的一次“邂逅”(附問題詳解)

【真實面試經歷】我和阿里面試官的一次“邂逅”(附問題詳解)

本文的內容都是根據讀者投稿的真實面試經歷改編而來,首次嘗試這種風格的文章,花了幾天晚上才總算寫完,希望對你有幫助。。已經收錄自 Guide 哥開源的 JavaGuide 中。
本文主要涵蓋下面的內容:

  1. 分散式商城系統:架構圖講解;
  2. 訊息佇列相關:削峰和解耦;
  3. Redis 相關:快取穿透問題的解決;
  4. 一些基礎問題:
    • 網路相關:1.瀏覽器輸入 URL 發生了什麼? 2.TCP 和 UDP 區別? 3.TCP 如何保證傳輸可靠性?
    • Java 基礎:1. 既然有了位元組流,為什麼還要有字元流? 2.深拷貝 和 淺拷貝有啥區別呢?

下面是正文!

面試開始,坐在我前面的就是這次我的面試官嗎?這髮量看著根本不像程式設計師啊?我心裡正嘀咕著,只聽見面試官說:“小夥,下午好,我今天就是你的面試官,咱們開始面試吧!”。

第一面開始

面試官: 我也不用多說了,你先自我介紹一下吧,簡歷上有的就不要再說了哈。

我: 內心 os:"果然如我所料,就知道會讓我先自我介紹一下,還好我看了 JavaGuide ,學到了一些套路。套路總結起來就是:最好準備好兩份自我介紹,一份對 hr 說的,主要講能突出自己的經歷,會的程式設計技術一語帶過;另一份對技術面試官說的,主要講自己會的技術細節,專案經驗,經歷那些就一語帶過。 所以,我按照這個套路準備了一個還算通用的模板,畢竟我懶嘛!不想多準備一個自我介紹,整個通用的多好!

面試官,您好!我叫小李子。大學時間我主要利用課外時間學習 Java 相關的知識。在校期間參與過一個某某系統的開發,主要負責資料庫設計和後端系統開發.,期間解決了什麼問題,巴拉巴拉。另外,我自己在學習過程中也參照網上的教程寫過一個電商系統的網站,寫這個電商網站主要是為了能讓自己接觸到分散式系統的開發。在學習之餘,我比較喜歡通過部落格整理分享自己所學知識。我現在已經是某社群的認證作者,寫過一系列關於 執行緒池使用以及原始碼分析的文章深受好評。另外,我獲得過省級程式設計比賽二等獎,我將這個獲獎專案開源到 Github 還收穫了 2k 的 Star 呢?

面試官: 你剛剛說參考網上的教程做了一個電商系統?你能畫畫這個電商系統的架構圖嗎?

我: 內心 os: "這可難不倒我!早知道寫在簡歷上的專案要重視了,提前都把這個系統的架構圖畫了好多遍了呢!"

做過分散式電商系統的一定很熟悉上面的架構圖(目前比較流行的是微服務架構,但是如果你有分散式開發經驗也是非常加分的!)。

面試官: 簡單介紹一下你做的這個系統吧!

我: 我一本正經的對著我剛剛畫的商城架構圖開始了滿嘴造火箭的講起來:

本系統主要分為展示層、服務層和持久層這三層。表現層顧名思義主要就是為了用來展示,比如我們的後臺管理系統的頁面、商城首頁的頁面、搜尋系統的頁面等等,這一層都只是作為展示,並沒有提供任何服務。

展示層和服務層一般是部署在不同的機器上來提高併發量和擴充套件性,那麼展示層和服務層怎樣才能互動呢?在本系統中我們使用 Dubbo 來進行服務治理。Dubbo 是一款高效能、輕量級的開源 Java RPC 框架。Dubbo 在本系統的主要作用就是提供遠端 RPC 呼叫。在本系統中服務層的資訊通過 Dubbo 註冊給 ZooKeeper,表現層通過 Dubbo 去 ZooKeeper 中獲取服務的相關資訊。Zookeeper 的作用僅僅是存放提供服務的伺服器的地址和一些服務的相關資訊,實現 RPC 遠端呼叫功能的還是 Dubbo。如果需要引用到某個服務的時候,我們只需要在配置檔案中配置相關資訊就可以在程式碼中直接使用了,就像呼叫本地方法一樣。假如說某個服務的使用量增加時,我們只用為這單個服務增加伺服器,而不需要為整個系統新增服務。

另外,本系統的資料庫使用的是常用的 MySQL,並且用到了資料庫中介軟體 MyCat。另外,本系統還用到 redis 記憶體資料庫來作為快取來提高系統的反應速度。假如使用者第一次訪問資料庫中的某些資料,這個過程會比較慢,因為是從硬碟上讀取的。將該使用者訪問的資料存在數快取中,這樣下一次再訪問這些資料的時候就可以直接從快取中獲取了。操作快取就是直接操作記憶體,所以速度相當快。

系統還用到了 Elasticsearch 來提供搜尋功能。使用 Elasticsearch 我們可以非常方便的為我們的商城系統新增必備的搜尋功能,並且使用 Elasticsearch 還能提供其它非常實用的功能,並且很容易擴充套件。

面試官: 我看你的系統裡面還用到了訊息佇列,能說說為什麼要用它嗎?

我:

使用訊息佇列主要是為了:

  1. 減少響應所需時間和削峰。
  2. 降低系統耦合性(解耦/提升系統可擴充套件性)。

面試官: 你這說的太簡單了!能不能稍微詳細一點,最好能畫圖給我解釋一下。

我: 內心 os:"都 2019 年了,大部分面試者都能對訊息佇列的為系統帶來的這兩個好處倒背如流了,如果你想走的更遠就要別別人懂的更深一點!"

當我們不使用訊息佇列的時候,所有的使用者的請求會直接落到伺服器,然後通過資料庫或者快取響應。假如在高併發的場景下,如果沒有快取或者資料庫承受不了這麼大的壓力的話,就會造成響應速度緩慢,甚至造成資料庫宕機。但是,在使用訊息佇列之後,使用者的請求資料傳送給了訊息佇列之後就可以立即返回,再由訊息佇列的消費者程序從訊息佇列中獲取資料,非同步寫入資料庫,不過要確保訊息不被重複消費還要考慮到訊息丟失問題。由於訊息佇列伺服器處理速度快於資料庫,因此響應速度得到大幅改善。

文字 is too 空洞,直接上圖吧!下圖展示了使用訊息前後系統處理使用者請求的對比(ps:我自己都被我畫的這個圖美到了,如果你也覺得這張圖好看的話麻煩來個素質三連!)。

通過以上分析我們可以得出訊息佇列具有很好的削峰作用的功能——即通過非同步處理,將短時間高併發產生的事務訊息儲存在訊息佇列中,從而削平高峰期的併發事務。 舉例:在電子商務一些秒殺、促銷活動中,合理使用訊息佇列可以有效抵禦促銷活動剛開始大量訂單湧入對系統的衝擊。如下圖所示:

使用訊息佇列還可以降低系統耦合性。我們知道如果模組之間不存在直接呼叫,那麼新增模組或者修改模組就對其他模組影響較小,這樣系統的可擴充套件性無疑更好一些。還是直接上圖吧:

生產者(客戶端)傳送訊息到訊息佇列中去,接受者(服務端)處理訊息,需要消費的系統直接去訊息佇列取訊息進行消費即可而不需要和其他系統有耦合, 這顯然也提高了系統的擴充套件性。

面試官: 你覺得它有什麼缺點嗎?或者說怎麼考慮用不用訊息佇列?

我: 內心 os: "面試官真雞賊!這不是勾引我上鉤麼?還好我準備充分。"

我覺得可以從下面幾個方面來說:

  1. 系統可用性降低: 系統可用性在某種程度上降低,為什麼這樣說呢?在加入 MQ 之前,你不用考慮訊息丟失或者說 MQ 掛掉等等的情況,但是,引入 MQ 之後你就需要去考慮了!
  2. 系統複雜性提高: 加入 MQ 之後,你需要保證訊息沒有被重複消費、處理訊息丟失的情況、保證訊息傳遞的順序性等等問題!
  3. 一致性問題: 我上面講了訊息佇列可以實現非同步,訊息佇列帶來的非同步確實可以提高系統響應速度。但是,萬一訊息的真正消費者並沒有正確消費訊息怎麼辦?這樣就會導致資料不一致的情況了!

面試官:做專案的過程中遇到了什麼問題嗎?解決了嗎?如果解決的話是如何解決的呢?

我 : 內心 os: "做的過程中好像也沒有遇到什麼問題啊!怎麼辦?怎麼辦?突然想到可以說我在使用 Redis 過程中遇到的問題,畢竟我對 Redis 還算熟悉嘛,把面試官往這個方向吸引,準沒錯。"

我在使用 Redis 對常用資料進行緩衝的過程中出現了快取穿透問題。然後,我通過谷歌搜尋相關的解決方案來解決的。

面試官: 你還知道快取穿透啊?不錯啊!來說說什麼是快取穿透以及你最後的解決辦法。

我: 我先來談談什麼是快取穿透吧!

快取穿透說簡單點就是大量請求的 key 根本不存在於快取中,導致請求直接到了資料庫上,根本沒有經過快取這一層。舉個例子:某個黑客故意製造我們快取中不存在的 key 發起大量請求,導致大量請求落到資料庫。

總結一下就是:

  1. 快取層不命中。
  2. 儲存層不命中,不將空結果寫回快取。
  3. 返回空結果給客戶端。

一般 MySQL 預設的最大連線數在 150 左右,這個可以通過 show variables like '%max_connections%';命令來檢視。最大連線數一個還只是一個指標,cpu,記憶體,磁碟,網路等物理條件都是其執行指標,這些指標都會限制其併發能力!所以,一般 3000 的併發請求就能打死大部分資料庫了。

面試官: 小夥子不錯啊!還準備問你:“為什麼 3000 的併發能把支援最大連線數 4000 資料庫壓死?”想不到你自己就提前回答了!不錯!

我: 別誇了!別誇了!我再來說說我知道的一些解決辦法以及我最後採用的方案吧!您幫忙看看有沒有問題。

最基本的就是首先做好引數校驗,一些不合法的引數請求直接丟擲異常資訊返回給客戶端。比如查詢的資料庫 id 不能小於 0、傳入的郵箱格式不對的時候直接返回錯誤訊息給客戶端等等。

引數校驗通過的情況還是會出現快取穿透,我們還可以通過以下幾個方案來解決這個問題:

1)快取無效 key : 如果快取和資料庫都查不到某個 key 的資料就寫一個到 redis 中去並設定過期時間,具體命令如下:SET key value EX 10086。這種方式可以解決請求的 key 變化不頻繁的情況,如何黑客惡意攻擊,每次構建的不同的請求 key,會導致 redis 中快取大量無效的 key 。很明顯,這種方案並不能從根本上解決此問題。如果非要用這種方式來解決穿透問題的話,儘量將無效的 key 的過期時間設定短一點比如 1 分鐘。

另外,這裡多說一嘴,一般情況下我們是這樣設計 key 的: 表名:列名:主鍵名:主鍵值

2)布隆過濾器: 布隆過濾器是一個非常神奇的資料結構,通過它我們可以非常方便地判斷一個給定資料是否存在於海量資料中。我們需要的就是判斷 key 是否合法,有沒有感覺布隆過濾器就是我們想要找的那個“人”。

面試官: 不錯不錯!你還知道布隆過濾器啊!來給我談一談。

我: 內心 os:“如果你準備過海量資料處理的面試題,你一定對:“如何確定一個數字是否在於包含大量數字的數字集中(數字集很大,5 億以上!)?”這個題目很瞭解了!解決這道題目就要用到布隆過濾器。”

布隆過濾器在針對海量資料去重或者驗證資料合法性的時候非常有用。布隆過濾器的本質實際上是 “位(bit)陣列”,也就是說每一個存入布隆過濾器的資料都只佔一位。相比於我們平時常用的的 List、Map 、Set 等資料結構,它佔用空間更少並且效率更高,但是缺點是其返回的結果是概率性的,而不是非常準確的。

當一個元素加入布隆過濾器中的時候,會進行如下操作:

  1. 使用布隆過濾器中的雜湊函式對元素值進行計算,得到雜湊值(有幾個雜湊函式得到幾個雜湊值)。
  2. 根據得到的雜湊值,在位陣列中把對應下標的值置為 1。

當我們需要判斷一個元素是否存在於布隆過濾器的時候,會進行如下操作:

  1. 對給定元素再次進行相同的雜湊計算;
  2. 得到值之後判斷位陣列中的每個元素是否都為 1,如果值都為 1,那麼說明這個值在布隆過濾器中,如果存在一個值不為 1,說明該元素不在布隆過濾器中。

舉個簡單的例子:

如圖所示,當字串儲存要加入到布隆過濾器中時,該字串首先由多個雜湊函式生成不同的雜湊值,然後在對應的位陣列的下表的元素設定為 1(當位陣列初始化時 ,所有位置均為 0)。當第二次儲存相同字串時,因為先前的對應位置已設定為 1,所以很容易知道此值已經存在(去重非常方便)。

如果我們需要判斷某個字串是否在布隆過濾器中時,只需要對給定字串再次進行相同的雜湊計算,得到值之後判斷位陣列中的每個元素是否都為 1,如果值都為 1,那麼說明這個值在布隆過濾器中,如果存在一個值不為 1,說明該元素不在布隆過濾器中。

不同的字串可能雜湊出來的位置相同,這種情況我們可以適當增加位陣列大小或者調整我們的雜湊函式。

綜上,我們可以得出:布隆過濾器說某個元素存在,小概率會誤判。布隆過濾器說某個元素不在,那麼這個元素一定不在。

面試官: 看來你對布隆過濾器瞭解的還挺不錯的嘛!那你快說說你最後是怎麼利用它來解決快取穿透的。

我: 知道了布隆過濾器的原理就之後就很容易做了。我是利用 Redis 布隆過濾器來做的。我把所有可能存在的請求的值都存放在布隆過濾器中,當用戶請求過來,我會先判斷使用者發來的請求的值是否存在於布隆過濾器中。不存在的話,直接返回請求引數錯誤資訊給客戶端,存在的話才會走下面的流程。總結一下就是下面這張圖(這張圖片不是我畫的,為了省事直接在網上找的):

更多關於布隆過濾器的內容可以看我的這篇原創:《不瞭解布隆過濾器?一文給你整的明明白白!》 ,強烈推薦,個人感覺網上應該找不到總結的這麼明明白白的文章了。

面試官: 好了好了。專案就暫時問到這裡吧!下面有一些比較基礎的問題我簡單地問一下你。內心 os: 難不成這傢伙滿口高併發,連最基礎的東西都不會吧!

我: 好的好的!沒問題!

面試官: 瀏覽器輸入 URL 發生了什麼?

我: 內心 os:“很常問的一個問題,建議拿小本本記好了!另外,百度好像最喜歡問這個問題,去百度面試可要提前備好這道題的功課哦!相似問題:開啟一個網頁,整個過程會使用哪些協議?”。

圖解(圖片來源:《圖解 HTTP》):

總體來說分為以下幾個過程:

  1. DNS 解析
  2. TCP 連線
  3. 傳送 HTTP 請求
  4. 伺服器處理請求並返回 HTTP 報文
  5. 瀏覽器解析渲染頁面
  6. 連線結束

具體可以參考下面這篇文章:

  • https://segmentfault.com/a/1190000006879700

面試官: TCP 和 UDP 區別?

我:

UDP 在傳送資料之前不需要先建立連線,遠地主機在收到 UDP 報文後,不需要給出任何確認。雖然 UDP 不提供可靠交付,但在某些情況下 UDP 確是一種最有效的工作方式(一般用於即時通訊),比如: QQ 語音、 QQ 視訊 、直播等等

TCP 提供面向連線的服務。在傳送資料之前必須先建立連線,資料傳送結束後要釋放連線。 TCP 不提供廣播或多播服務。由於 TCP 要提供可靠的,面向連線的傳輸服務(TCP 的可靠體現在 TCP 在傳遞資料之前,會有三次握手來建立連線,而且在資料傳遞時,有確認、視窗、重傳、擁塞控制機制,在資料傳完後,還會斷開連線用來節約系統資源),這一難以避免增加了許多開銷,如確認,流量控制,計時器以及連線管理等。這不僅使協議資料單元的首部增大很多,還要佔用許多處理機資源。TCP 一般用於檔案傳輸、傳送和接收郵件、遠端登入等場景。

面試官: TCP 如何保證傳輸可靠性?

我:

  1. 應用資料被分割成 TCP 認為最適合傳送的資料塊。
  2. TCP 給傳送的每一個包進行編號,接收方對資料包進行排序,把有序資料傳送給應用層。
  3. 校驗和: TCP 將保持它首部和資料的檢驗和。這是一個端到端的檢驗和,目的是檢測資料在傳輸過程中的任何變化。如果收到段的檢驗和有差錯,TCP 將丟棄這個報文段和不確認收到此報文段。
  4. TCP 的接收端會丟棄重複的資料。
  5. 流量控制: TCP 連線的每一方都有固定大小的緩衝空間,TCP 的接收端只允許傳送端傳送接收端緩衝區能接納的資料。當接收方來不及處理髮送方的資料,能提示傳送方降低傳送的速率,防止包丟失。TCP 使用的流量控制協議是可變大小的滑動視窗協議。 (TCP 利用滑動視窗實現流量控制)
  6. 擁塞控制: 當網路擁塞時,減少資料的傳送。
  7. ARQ 協議: 也是為了實現可靠傳輸的,它的基本原理就是每發完一個分組就停止傳送,等待對方確認。在收到確認後再發下一個分組。
  8. 超時重傳: 當 TCP 發出一個段後,它啟動一個定時器,等待目的端確認收到這個報文段。如果不能及時收到一個確認,將重發這個報文段。

面試官: 我再來問你一些 Java 基礎的問題吧!小夥子。

我: 好的。(內心 os:“你儘管來!”)

面試官: 既然有了位元組流,為什麼還要有字元流?

我:內心 os :“問題本質想問:不管是檔案讀寫還是網路傳送接收,資訊的最小儲存單元都是位元組,那為什麼 I/O 流操作要分為位元組流操作和字元流操作呢?”

字元流是由 Java 虛擬機器將位元組轉換得到的,問題就出在這個過程還算是非常耗時,並且,如果我們不知道編碼型別就很容易出現亂碼問題。所以, I/O 流就乾脆提供了一個直接操作字元的介面,方便我們平時對字元進行流操作。如果音訊檔案、圖片等媒體檔案用位元組流比較好,如果涉及到字元的話使用字元流比較好。

面試官:深拷貝 和 淺拷貝有啥區別呢?

我:

  1. 淺拷貝:對基本資料型別進行值傳遞,對引用資料型別進行引用傳遞般的拷貝,此為淺拷貝。
  2. 深拷貝:對基本資料型別進行值傳遞,對引用資料型別,建立一個新的物件,並複製其內容,此為深拷貝。

面試官: 好的!面試結束。小夥子可以的!回家等通知吧!

我: 好的好的!辛苦您了!