本文的內容都是根據讀者投稿的真實面試經歷改編而來,首次嘗試這種風格的文章,花了幾天晚上才總算寫完,希望對你有幫助。。已經收錄自 Guide 哥開源的 JavaGuide 中。
本文主要涵蓋下面的內容:
- 分布式商城系統:架構圖講解;
- 消息隊列相關:削峰和解耦;
- Redis 相關:緩存穿透問題的解決;
- 一些基礎問題:
- 網絡相關: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 還能提供其它非常實用的功能,並且很容易擴展。
面試官: 我看你的系統里面還用到了消息隊列,能說說為什么要用它嗎?
我:
使用消息隊列主要是為了:
- 減少響應所需時間和削峰。
- 降低系統耦合性(解耦/提升系統可擴展性)。
面試官: 你這說的太簡單了!能不能稍微詳細一點,最好能畫圖給我解釋一下。
我: 內心 os:"都 2019 年了,大部分面試者都能對消息隊列的為系統帶來的這兩個好處倒背如流了,如果你想走的更遠就要別別人懂的更深一點!"
當我們不使用消息隊列的時候,所有的用戶的請求會直接落到服務器,然后通過數據庫或者緩存響應。假如在高並發的場景下,如果沒有緩存或者數據庫承受不了這么大的壓力的話,就會造成響應速度緩慢,甚至造成數據庫宕機。但是,在使用消息隊列之后,用戶的請求數據發送給了消息隊列之后就可以立即返回,再由消息隊列的消費者進程從消息隊列中獲取數據,異步寫入數據庫,不過要確保消息不被重復消費還要考慮到消息丟失問題。由於消息隊列服務器處理速度快於數據庫,因此響應速度得到大幅改善。
文字 is too 空洞,直接上圖吧!下圖展示了使用消息前后系統處理用戶請求的對比(ps:我自己都被我畫的這個圖美到了,如果你也覺得這張圖好看的話麻煩來個素質三連!)。
通過以上分析我們可以得出消息隊列具有很好的削峰作用的功能——即通過異步處理,將短時間高並發產生的事務消息存儲在消息隊列中,從而削平高峰期的並發事務。 舉例:在電子商務一些秒殺、促銷活動中,合理使用消息隊列可以有效抵御促銷活動剛開始大量訂單涌入對系統的沖擊。如下圖所示:
使用消息隊列還可以降低系統耦合性。我們知道如果模塊之間不存在直接調用,那么新增模塊或者修改模塊就對其他模塊影響較小,這樣系統的可擴展性無疑更好一些。還是直接上圖吧:
生產者(客戶端)發送消息到消息隊列中去,接受者(服務端)處理消息,需要消費的系統直接去消息隊列取消息進行消費即可而不需要和其他系統有耦合, 這顯然也提高了系統的擴展性。
面試官: 你覺得它有什么缺點嗎?或者說怎么考慮用不用消息隊列?
我: 內心 os: "面試官真雞賊!這不是勾引我上鈎么?還好我准備充分。"
我覺得可以從下面幾個方面來說:
- 系統可用性降低: 系統可用性在某種程度上降低,為什么這樣說呢?在加入 MQ 之前,你不用考慮消息丟失或者說 MQ 掛掉等等的情況,但是,引入 MQ 之后你就需要去考慮了!
- 系統復雜性提高: 加入 MQ 之后,你需要保證消息沒有被重復消費、處理消息丟失的情況、保證消息傳遞的順序性等等問題!
- 一致性問題: 我上面講了消息隊列可以實現異步,消息隊列帶來的異步確實可以提高系統響應速度。但是,萬一消息的真正消費者並沒有正確消費消息怎么辦?這樣就會導致數據不一致的情況了!
面試官:做項目的過程中遇到了什么問題嗎?解決了嗎?如果解決的話是如何解決的呢?
我 : 內心 os: "做的過程中好像也沒有遇到什么問題啊!怎么辦?怎么辦?突然想到可以說我在使用 Redis 過程中遇到的問題,畢竟我對 Redis 還算熟悉嘛,把面試官往這個方向吸引,准沒錯。"
我在使用 Redis 對常用數據進行緩沖的過程中出現了緩存穿透問題。然后,我通過谷歌搜索相關的解決方案來解決的。
面試官: 你還知道緩存穿透啊?不錯啊!來說說什么是緩存穿透以及你最后的解決辦法。
我: 我先來談談什么是緩存穿透吧!
緩存穿透說簡單點就是大量請求的 key 根本不存在於緩存中,導致請求直接到了數據庫上,根本沒有經過緩存這一層。舉個例子:某個黑客故意制造我們緩存中不存在的 key 發起大量請求,導致大量請求落到數據庫。
總結一下就是:
- 緩存層不命中。
- 存儲層不命中,不將空結果寫回緩存。
- 返回空結果給客戶端。
一般 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。
當我們需要判斷一個元素是否存在於布隆過濾器的時候,會進行如下操作:
- 對給定元素再次進行相同的哈希計算;
- 得到值之后判斷位數組中的每個元素是否都為 1,如果值都為 1,那么說明這個值在布隆過濾器中,如果存在一個值不為 1,說明該元素不在布隆過濾器中。
舉個簡單的例子:
如圖所示,當字符串存儲要加入到布隆過濾器中時,該字符串首先由多個哈希函數生成不同的哈希值,然后在對應的位數組的下表的元素設置為 1(當位數組初始化時 ,所有位置均為 0)。當第二次存儲相同字符串時,因為先前的對應位置已設置為 1,所以很容易知道此值已經存在(去重非常方便)。
如果我們需要判斷某個字符串是否在布隆過濾器中時,只需要對給定字符串再次進行相同的哈希計算,得到值之后判斷位數組中的每個元素是否都為 1,如果值都為 1,那么說明這個值在布隆過濾器中,如果存在一個值不為 1,說明該元素不在布隆過濾器中。
不同的字符串可能哈希出來的位置相同,這種情況我們可以適當增加位數組大小或者調整我們的哈希函數。
綜上,我們可以得出:布隆過濾器說某個元素存在,小概率會誤判。布隆過濾器說某個元素不在,那么這個元素一定不在。
面試官: 看來你對布隆過濾器了解的還挺不錯的嘛!那你快說說你最后是怎么利用它來解決緩存穿透的。
我: 知道了布隆過濾器的原理就之后就很容易做了。我是利用 Redis 布隆過濾器來做的。我把所有可能存在的請求的值都存放在布隆過濾器中,當用戶請求過來,我會先判斷用戶發來的請求的值是否存在於布隆過濾器中。不存在的話,直接返回請求參數錯誤信息給客戶端,存在的話才會走下面的流程。總結一下就是下面這張圖(這張圖片不是我畫的,為了省事直接在網上找的):

更多關於布隆過濾器的內容可以看我的這篇原創:《不了解布隆過濾器?一文給你整的明明白白!》 ,強烈推薦,個人感覺網上應該找不到總結的這么明明白白的文章了。
面試官: 好了好了。項目就暫時問到這里吧!下面有一些比較基礎的問題我簡單地問一下你。內心 os: 難不成這家伙滿口高並發,連最基礎的東西都不會吧!
我: 好的好的!沒問題!
面試官: 瀏覽器輸入 URL 發生了什么?
我: 內心 os:“很常問的一個問題,建議拿小本本記好了!另外,百度好像最喜歡問這個問題,去百度面試可要提前備好這道題的功課哦!相似問題:打開一個網頁,整個過程會使用哪些協議?”。
圖解(圖片來源:《圖解 HTTP》):
![]()
總體來說分為以下幾個過程:
- DNS 解析
- TCP 連接
- 發送 HTTP 請求
- 服務器處理請求並返回 HTTP 報文
- 瀏覽器解析渲染頁面
- 連接結束
具體可以參考下面這篇文章:
面試官: TCP 和 UDP 區別?
我:
UDP 在傳送數據之前不需要先建立連接,遠地主機在收到 UDP 報文后,不需要給出任何確認。雖然 UDP 不提供可靠交付,但在某些情況下 UDP 確是一種最有效的工作方式(一般用於即時通信),比如: QQ 語音、 QQ 視頻 、直播等等
TCP 提供面向連接的服務。在傳送數據之前必須先建立連接,數據傳送結束后要釋放連接。 TCP 不提供廣播或多播服務。由於 TCP 要提供可靠的,面向連接的傳輸服務(TCP 的可靠體現在 TCP 在傳遞數據之前,會有三次握手來建立連接,而且在數據傳遞時,有確認、窗口、重傳、擁塞控制機制,在數據傳完后,還會斷開連接用來節約系統資源),這一難以避免增加了許多開銷,如確認,流量控制,計時器以及連接管理等。這不僅使協議數據單元的首部增大很多,還要占用許多處理機資源。TCP 一般用於文件傳輸、發送和接收郵件、遠程登錄等場景。
面試官: TCP 如何保證傳輸可靠性?
我:
- 應用數據被分割成 TCP 認為最適合發送的數據塊。
- TCP 給發送的每一個包進行編號,接收方對數據包進行排序,把有序數據傳送給應用層。
- 校驗和: TCP 將保持它首部和數據的檢驗和。這是一個端到端的檢驗和,目的是檢測數據在傳輸過程中的任何變化。如果收到段的檢驗和有差錯,TCP 將丟棄這個報文段和不確認收到此報文段。
- TCP 的接收端會丟棄重復的數據。
- 流量控制: TCP 連接的每一方都有固定大小的緩沖空間,TCP 的接收端只允許發送端發送接收端緩沖區能接納的數據。當接收方來不及處理發送方的數據,能提示發送方降低發送的速率,防止包丟失。TCP 使用的流量控制協議是可變大小的滑動窗口協議。 (TCP 利用滑動窗口實現流量控制)
- 擁塞控制: 當網絡擁塞時,減少數據的發送。
- ARQ 協議: 也是為了實現可靠傳輸的,它的基本原理就是每發完一個分組就停止發送,等待對方確認。在收到確認后再發下一個分組。
- 超時重傳: 當 TCP 發出一個段后,它啟動一個定時器,等待目的端確認收到這個報文段。如果不能及時收到一個確認,將重發這個報文段。
面試官: 我再來問你一些 Java 基礎的問題吧!小伙子。
我: 好的。(內心 os:“你盡管來!”)
面試官: 既然有了字節流,為什么還要有字符流?
我:內心 os :“問題本質想問:不管是文件讀寫還是網絡發送接收,信息的最小存儲單元都是字節,那為什么 I/O 流操作要分為字節流操作和字符流操作呢?”
字符流是由 Java 虛擬機將字節轉換得到的,問題就出在這個過程還算是非常耗時,並且,如果我們不知道編碼類型就很容易出現亂碼問題。所以, I/O 流就干脆提供了一個直接操作字符的接口,方便我們平時對字符進行流操作。如果音頻文件、圖片等媒體文件用字節流比較好,如果涉及到字符的話使用字符流比較好。
面試官:深拷貝 和 淺拷貝有啥區別呢?
我:
- 淺拷貝:對基本數據類型進行值傳遞,對引用數據類型進行引用傳遞般的拷貝,此為淺拷貝。
- 深拷貝:對基本數據類型進行值傳遞,對引用數據類型,創建一個新的對象,並復制其內容,此為深拷貝。
面試官: 好的!面試結束。小伙子可以的!回家等通知吧!
我: 好的好的!辛苦您了!