【備戰春招/秋招系列】美團Java面經總結進階篇 (附詳解答案)

SnailClimb發表於2018-11-14

【備戰春招/秋招系列】美團面經總結進階篇 (附詳解答案)

該文已加入開源文件:JavaGuide(一份涵蓋大部分Java程式設計師所需要掌握的核心知識)。地址:github.com/Snailclimb/….

系列文章:

這是我總結的美團面經的進階篇,後面還有終結篇哦!下面只是我從很多份美團面經中總結的在美團面試中一些常見的問題。不同於個人面經,這份面經具有普適性。每次面試必備的自我介紹、專案介紹這些東西,大家可以自己私下好好思考。我在前面的文章中也提到了應該怎麼做自我介紹與專案介紹,詳情可以檢視這篇文章:【備戰春招/秋招系列2】初出茅廬的程式設計師該如何準備面試?

有人私信我讓我對美團面試難度做一個評級,我覺得如果有10級的話,美團面試的難度大概在6級左右吧!部分情況可能因人而異了。

訊息佇列/訊息中介軟體應該是Java程式設計師必備的一個技能了,如果你之前沒接觸過訊息佇列的話,建議先去百度一下某某訊息佇列入門,然後花2個小時就差不多可以學會任何一種訊息佇列的使用了。如果說僅僅學會使用是萬萬不夠的,在實際生產環境還要考慮訊息丟失等等情況。關於訊息佇列面試相關的問題,推薦大家也可以看一下視訊《Java工程師面試突擊第1季-中華石杉老師》,如果大家沒有資源的話,可以在我的公眾號“Java面試通關手冊”後臺回覆關鍵字“1”即可!

一 訊息佇列MQ的套路

面試官一般會先問你這個問題,預熱一下,看你知道訊息佇列不,一般在第一面的時候面試官可能只會問訊息佇列MQ的應用場景/使用訊息佇列的好處、使用訊息佇列會帶來什麼問題、訊息佇列的技術選型這幾個問題,不會太深究下去,在後面的第二輪/第三輪技術面試中可能會深入問一下。

1.1 介紹一下訊息佇列MQ的應用場景/使用訊息佇列的好處

《大型網站技術架構》第四章和第七章均有提到訊息佇列對應用效能及擴充套件性的提升。

①.通過非同步處理提高系統效能

通過非同步處理提高系統效能
如上圖,在不使用訊息佇列伺服器的時候,使用者的請求資料直接寫入資料庫,在高併發的情況下資料庫壓力劇增,使得響應速度變慢。但是在使用訊息佇列之後,使用者的請求資料傳送給訊息佇列之後立即 返回,再由訊息佇列的消費者程式從訊息佇列中獲取資料,非同步寫入資料庫。由於訊息佇列伺服器處理速度快於資料庫(訊息佇列也比資料庫有更好的伸縮性),因此響應速度得到大幅改善。

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

合理使用訊息佇列可以有效抵禦促銷活動剛開始大量訂單湧入對系統的衝擊
因為使用者請求資料寫入訊息佇列之後就立即返回給使用者了,但是請求資料在後續的業務校驗、寫資料庫等操作中可能失敗。因此使用訊息佇列進行非同步處理之後,需要適當修改業務流程進行配合,比如使用者在提交訂單之後,訂單資料寫入訊息佇列,不能立即返回使用者訂單提交成功,需要在訊息佇列的訂單消費者程式真正處理完該訂單之後,甚至出庫後,再通過電子郵件或簡訊通知使用者訂單成功,以免交易糾紛。這就類似我們平時手機訂火車票和電影票。

②.降低系統耦合性

我們知道模組分散式部署以後聚合方式通常有兩種:1.分散式訊息佇列和2.分散式服務

先來簡單說一下分散式服務:

目前使用比較多的用來構建SOA(Service Oriented Architecture面向服務體系結構)分散式服務框架是阿里巴巴開源的Dubbo.如果想深入瞭解Dubbo的可以看我寫的關於Dubbo的這一篇文章:《高效能優秀的服務框架-dubbo介紹》juejin.im/post/5acade…

再來談我們的分散式訊息佇列:

我們知道如果模組之間不存在直接呼叫,那麼新增模組或者修改模組就對其他模組影響較小,這樣系統的可擴充套件性無疑更好一些。

我們最常見的事件驅動架構類似生產者消費者模式,在大型網站中通常用利用訊息佇列實現事件驅動結構。如下圖所示:

利用訊息佇列實現事件驅動結構
訊息佇列使利用釋出-訂閱模式工作,訊息傳送者(生產者)釋出訊息,一個或多個訊息接受者(消費者)訂閱訊息。 從上圖可以看到訊息傳送者(生產者)和訊息接受者(消費者)之間沒有直接耦合,訊息傳送者將訊息傳送至分散式訊息佇列即結束對訊息的處理,訊息接受者從分散式訊息佇列獲取該訊息後進行後續處理,並不需要知道該訊息從何而來。對新增業務,只要對該類訊息感興趣,即可訂閱該訊息,對原有系統和業務沒有任何影響,從而實現網站業務的可擴充套件性設計

訊息接受者對訊息進行過濾、處理、包裝後,構造成一個新的訊息型別,將訊息繼續傳送出去,等待其他訊息接受者訂閱該訊息。因此基於事件(訊息物件)驅動的業務架構可以是一系列流程。

另外為了避免訊息佇列伺服器當機造成訊息丟失,會將成功傳送到訊息佇列的訊息儲存在訊息生產者伺服器上,等訊息真正被消費者伺服器處理後才刪除訊息。在訊息佇列伺服器當機後,生產者伺服器會選擇分散式訊息佇列伺服器叢集中的其他伺服器釋出訊息。

備註: 不要認為訊息佇列只能利用釋出-訂閱模式工作,只不過在解耦這個特定業務環境下是使用釋出-訂閱模式的,比如在我們的ActiveMQ訊息佇列中還有點對點工作模式,具體的會在後面的文章給大家詳細介紹,這一篇文章主要還是讓大家對訊息佇列有一個更透徹的瞭解。

這個問題一般會在上一個問題問完之後,緊接著被問到。“使用訊息佇列會帶來什麼問題?”這個問題要引起重視,一般我們都會考慮使用訊息佇列會帶來的好處而忽略它帶來的問題!

1.2 那麼使用訊息佇列會帶來什麼問題?考慮過這個問題嗎?

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

瞭解下面這個問題是為了我們更好的進行技術選型!該部分摘自:《Java工程師面試突擊第1季-中華石杉老師》,如果大家沒有資源的話,可以在我的公眾號“Java面試通關手冊”後臺回覆關鍵字“1”即可!

1.3 介紹一下你知道哪幾種訊息佇列,該如何選擇呢?

特性 ActiveMQ RabbitMQ RocketMQ Kafaka
單機吞吐量 萬級,吞吐量比RocketMQ和Kafka要低了一個數量級 萬級,吞吐量比RocketMQ和Kafka要低了一個數量級 10萬級,RocketMQ也是可以支撐高吞吐的一種MQ 10萬級別,這是kafka最大的優點,就是吞吐量高。一般配合大資料類的系統來進行實時資料計算、日誌採集等場景
topic數量對吞吐量的影響 topic可以達到幾百,幾千個的級別,吞吐量會有較小幅度的下降這是RocketMQ的一大優勢,在同等機器下,可以支撐大量的topic topic從幾十個到幾百個的時候,吞吐量會大幅度下降。所以在同等機器下,kafka儘量保證topic數量不要過多。如果要支撐大規模topic,需要增加更多的機器資源
可用性 高,基於主從架構實現高可用性 高,基於主從架構實現高可用性 非常高,分散式架構 非常高,kafka是分散式的,一個資料多個副本,少數機器當機,不會丟失資料,不會導致不可用
訊息可靠性 有較低的概率丟失資料 經過引數優化配置,可以做到0丟失 經過引數優化配置,訊息可以做到0丟失
時效性 ms級 微秒級,這是rabbitmq的一大特點,延遲是最低的 ms級 延遲在ms級以內
功能支援 MQ領域的功能極其完備 基於erlang開發,所以併發能力很強,效能極其好,延時很低 MQ功能較為完善,還是分散式的,擴充套件性好 功能較為簡單,主要支援簡單的MQ功能,在大資料領域的實時計算以及日誌採集被大規模使用,是事實上的標準
優劣勢總結 非常成熟,功能強大,在業內大量的公司以及專案中都有應用。偶爾會有較低概率丟失訊息,而且現在社群以及國內應用都越來越少,官方社群現在對ActiveMQ 5.x維護越來越少,幾個月才釋出一個版本而且確實主要是基於解耦和非同步來用的,較少在大規模吞吐的場景中使用 erlang語言開發,效能極其好,延時很低;吞吐量到萬級,MQ功能比較完備而且開源提供的管理介面非常棒,用起來很好用。社群相對比較活躍,幾乎每個月都發布幾個版本分在國內一些網際網路公司近幾年用rabbitmq也比較多一些但是問題也是顯而易見的,RabbitMQ確實吞吐量會低一些,這是因為他做的實現機制比較重。而且erlang開發,國內有幾個公司有實力做erlang原始碼級別的研究和定製?如果說你沒這個實力的話,確實偶爾會有一些問題,你很難去看懂原始碼,你公司對這個東西的掌控很弱,基本職能依賴於開源社群的快速維護和修復bug。而且rabbitmq叢集動態擴充套件會很麻煩,不過這個我覺得還好。其實主要是erlang語言本身帶來的問題。很難讀原始碼,很難定製和掌控。 介面簡單易用,而且畢竟在阿里大規模應用過,有阿里品牌保障。日處理訊息上百億之多,可以做到大規模吞吐,效能也非常好,分散式擴充套件也很方便,社群維護還可以,可靠性和可用性都是ok的,還可以支撐大規模的topic數量,支援複雜MQ業務場景。而且一個很大的優勢在於,阿里出品都是java系的,我們可以自己閱讀原始碼,定製自己公司的MQ,可以掌控。社群活躍度相對較為一般,不過也還可以,文件相對來說簡單一些,然後介面這塊不是按照標準JMS規範走的有些系統要遷移需要修改大量程式碼。還有就是阿里出臺的技術,你得做好這個技術萬一被拋棄,社群黃掉的風險,那如果你們公司有技術實力我覺得用RocketMQ挺好的 kafka的特點其實很明顯,就是僅僅提供較少的核心功能,但是提供超高的吞吐量,ms級的延遲,極高的可用性以及可靠性,而且分散式可以任意擴充套件。同時kafka最好是支撐較少的topic數量即可,保證其超高吞吐量。而且kafka唯一的一點劣勢是有可能訊息重複消費,那麼對資料準確性會造成極其輕微的影響,在大資料領域中以及日誌採集中,這點輕微影響可以忽略這個特性天然適合大資料實時計算以及日誌收集。

這部分內容,我這裡不給出答案,大家可以自行根據自己學習的訊息佇列查閱相關內容,我可能會在後面的文章中介紹到這部分內容。另外,下面這些問題在視訊《Java工程師面試突擊第1季-中華石杉老師》中都有提到,如果大家沒有資源的話,可以在我的公眾號“Java面試通關手冊”後臺回覆關鍵字“1”即可!

1.4 關於訊息佇列其他一些常見的問題展望

  1. 引入訊息佇列之後如何保證高可用性
  2. 如何保證訊息不被重複消費呢?
  3. 如何保證訊息的可靠性傳輸(如何處理訊息丟失的問題)?
  4. 我該怎麼保證從訊息佇列裡拿到的資料按順序執行?
  5. 如何解決訊息佇列的延時以及過期失效問題?訊息佇列滿了以後該怎麼處理?有幾百萬訊息持續積壓幾小時,說說怎麼解決?
  6. 如果讓你來開發一個訊息佇列中介軟體,你會怎麼設計架構?

二 談談 InnoDB 和 MyIsam 兩者的區別

2.1 兩者的對比

  1. count運算上的區別: 因為MyISAM快取有表meta-data(行數等),因此在做COUNT(*)時對於一個結構很好的查詢是不需要消耗多少資源的。而對於InnoDB來說,則沒有這種快取。

  2. 是否支援事務和崩潰後的安全恢復: MyISAM 強調的是效能,每次查詢具有原子性,其執行數度比InnoDB型別更快,但是不提供事務支援。但是InnoDB 提供事務支援事務,外部鍵等高階資料庫功能。 具有事務(commit)、回滾(rollback)和崩潰修復能力(crash recovery capabilities)的事務安全(transaction-safe (ACID compliant))型表。

3)是否支援外來鍵: MyISAM不支援,而InnoDB支援。

2.2 關於兩者的總結

MyISAM更適合讀密集的表,而InnoDB更適合寫密集的的表。 在資料庫做主從分離的情況下,經常選擇MyISAM作為主庫的儲存引擎。

一般來說,如果需要事務支援,並且有較高的併發讀取頻率(MyISAM的表鎖的粒度太大,所以當該表寫併發量較高時,要等待的查詢就會很多了),InnoDB是不錯的選擇。如果你的資料量很大(MyISAM支援壓縮特性可以減少磁碟的空間佔用),而且不需要支援事務時,MyISAM是最好的選擇。

三 聊聊 Java 中的集合吧!

3.1 Arraylist 與 LinkedList 有什麼不同?(注意加上從資料結構分析的內容)

  • 1. 是否保證執行緒安全: ArrayList 和 LinkedList 都是不同步的,也就是不保證執行緒安全;
  • 2. 底層資料結構: Arraylist 底層使用的是Object陣列;LinkedList 底層使用的是雙向連結串列資料結構(注意雙向連結串列和雙向迴圈連結串列的區別:);
  • 3. 插入和刪除是否受元素位置的影響:ArrayList 採用陣列儲存,所以插入和刪除元素的時間複雜度受元素位置的影響。 比如:執行add(E e)方法的時候, ArrayList 會預設在將指定的元素追加到此列表的末尾,這種情況時間複雜度就是O(1)。但是如果要在指定位置 i 插入和刪除元素的話(add(int index, E element))時間複雜度就為 O(n-i)。因為在進行上述操作的時候集合中第 i 和第 i 個元素之後的(n-i)個元素都要執行向後位/向前移一位的操作。 ② LinkedList 採用連結串列儲存,所以插入,刪除元素時間複雜度不受元素位置的影響,都是近似 O(1)而陣列為近似 O(n)。
  • 4. 是否支援快速隨機訪問: LinkedList 不支援高效的隨機元素訪問,而 ArrayList 支援。快速隨機訪問就是通過元素的序號快速獲取元素物件(對應於get(int index)方法)。
  • 5. 記憶體空間佔用: ArrayList的空 間浪費主要體現在在list列表的結尾會預留一定的容量空間,而LinkedList的空間花費則體現在它的每一個元素都需要消耗比ArrayList更多的空間(因為要存放直接後繼和直接前驅以及資料)。

補充內容:RandomAccess介面

public interface RandomAccess {
}
複製程式碼

檢視原始碼我們發現實際上 RandomAccess 介面中什麼都沒有定義。所以,在我看來 RandomAccess 介面不過是一個標識罷了。標識什麼? 標識實現這個介面的類具有隨機訪問功能。

在binarySearch()方法中,它要判斷傳入的list 是否RamdomAccess的例項,如果是,呼叫indexedBinarySearch()方法,如果不是,那麼呼叫iteratorBinarySearch()方法

    public static <T>
    int binarySearch(List<? extends Comparable<? super T>> list, T key) {
        if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
            return Collections.indexedBinarySearch(list, key);
        else
            return Collections.iteratorBinarySearch(list, key);
    }

複製程式碼

ArraysList 實現了 RandomAccess 介面, 而 LinkedList 沒有實現。為什麼呢?我覺得還是和底層資料結構有關!ArraysList 底層是陣列,而 LinkedList 底層是連結串列。陣列天然支援隨機訪問,時間複雜度為 O(1),所以稱為快速隨機訪問。連結串列需要遍歷到特定位置才能訪問特定位置的元素,時間複雜度為 O(n),所以不支援快速隨機訪問。,ArraysList 實現了 RandomAccess 介面,就表明了他具有快速隨機訪問功能。 RandomAccess 介面只是標識,並不是說 ArraysList 實現 RandomAccess 介面才具有快速隨機訪問功能的!

下面再總結一下 list 的遍歷方式選擇:

  • 實現了RadmoAcces介面的list,優先選擇普通for迴圈 ,其次foreach,
  • 未實現RadmoAcces介面的ist, 優先選擇iterator遍歷(foreach遍歷底層也是通過iterator實現的),大size的資料,千萬不要使用普通for迴圈

Java 中的集合這類問題幾乎是面試必問的,問到這類問題的時候,HashMap 又是幾乎必問的問題,所以大家一定要引起重視!

3.2 HashMap的底層實現

####① JDK1.8之前

JDK1.8 之前 HashMap 底層是 陣列和連結串列 結合在一起使用也就是 連結串列雜湊HashMap 通過 key 的 hashCode 經過擾動函式處理過後得到 hash 值,然後通過 (n - 1) & hash 判斷當前元素存放的位置(這裡的 n 指的時陣列的長度),如果當前位置存在元素的話,就判斷該元素與要存入的元素的 hash 值以及 key 是否相同,如果相同的話,直接覆蓋,不相同就通過拉鍊法解決衝突。

所謂擾動函式指的就是 HashMap 的 hash 方法。使用 hash 方法也就是擾動函式是為了防止一些實現比較差的 hashCode() 方法 換句話說使用擾動函式之後可以減少碰撞。

JDK 1.8 HashMap 的 hash 方法原始碼:

JDK 1.8 的 hash方法 相比於 JDK 1.7 hash 方法更加簡化,但是原理不變。

      static final int hash(Object key) {
        int h;
        // key.hashCode():返回雜湊值也就是hashcode
        // ^ :按位異或
        // >>>:無符號右移,忽略符號位,空位都以0補齊
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
複製程式碼

對比一下 JDK1.7的 HashMap 的 hash 方法原始碼.

static int hash(int h) {
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).

    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
複製程式碼

相比於 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的效能會稍差一點點,因為畢竟擾動了 4 次。

所謂 “拉鍊法” 就是:將連結串列和陣列相結合。也就是說建立一個連結串列陣列,陣列中每一格就是一個連結串列。若遇到雜湊衝突,則將衝突的值加到連結串列中即可。

jdk1.8之前的內部結構

###② JDK1.8之後

相比於之前的版本, JDK1.8之後在解決雜湊衝突時有了較大的變化,當連結串列長度大於閾值(預設為8)時,將連結串列轉化為紅黑樹,以減少搜尋時間。

JDK1.8之後的HashMap底層資料結構

TreeMap、TreeSet以及JDK1.8之後的HashMap底層都用到了紅黑樹。紅黑樹就是為了解決二叉查詢樹的缺陷,因為二叉查詢樹在某些情況下會退化成一個線性結構。

問完 HashMap 的底層原理之後,面試官可能就會緊接著問你 HashMap 底層資料結構相關的問題!

3.3 既然談到了紅黑樹,你給我手繪一個出來吧,然後簡單講一下自己對於紅黑樹的理解

紅黑樹

紅黑樹特點:

  1. 每個節點非紅即黑;
  2. 根節點總是黑色的;
  3. 每個葉子節點都是黑色的空節點(NIL節點);
  4. 如果節點是紅色的,則它的子節點必須是黑色的(反之不一定);
  5. 從根節點到葉節點或空子節點的每條路徑,必須包含相同數目的黑色節點(即相同的黑色高度)

紅黑樹的應用:

TreeMap、TreeSet以及JDK1.8之後的HashMap底層都用到了紅黑樹。

為什麼要用紅黑樹

簡單來說紅黑樹就是為了解決二叉查詢樹的缺陷,因為二叉查詢樹在某些情況下會退化成一個線性結構。

3.4 紅黑樹這麼優秀,為何不直接使用紅黑樹得了?

說一下自己對於這個問題的看法:我們知道紅黑樹屬於(自)平衡二叉樹,但是為了保持“平衡”是需要付出代價的,紅黑樹在插入新資料後可能需要通過左旋,右旋、變色這些操作來保持平衡,這費事啊。你說說我們引入紅黑樹就是為了查詢資料快,如果連結串列長度很短的話,根本不需要引入紅黑樹的,你引入之後還要付出代價維持它的平衡。但是連結串列過長就不一樣了。至於為什麼選 8 這個值呢?通過概率統計所得,這個值是綜合查詢成本和新增元素成本得出的最好的一個值。

3.5 HashMap 和 Hashtable 的區別/HashSet 和 HashMap 區別

HashMap 和 Hashtable 的區別

  1. 執行緒是否安全: HashMap 是非執行緒安全的,HashTable 是執行緒安全的;HashTable 內部的方法基本都經過 synchronized 修飾。(如果你要保證執行緒安全的話就使用 ConcurrentHashMap 吧!);
  2. 效率: 因為執行緒安全的問題,HashMap 要比 HashTable 效率高一點。另外,HashTable 基本被淘汰,不要在程式碼中使用它;
  3. 對Null key 和Null value的支援: HashMap 中,null 可以作為鍵,這樣的鍵只有一個,可以有一個或多個鍵所對應的值為 null。。但是在 HashTable 中 put 進的鍵值只要有一個 null,直接丟擲 NullPointerException。
  4. 初始容量大小和每次擴充容量大小的不同 : ①建立時如果不指定容量初始值,Hashtable 預設的初始大小為11,之後每次擴充,容量變為原來的2n+1。HashMap 預設的初始化大小為16。之後每次擴充,容量變為原來的2倍。②建立時如果給定了容量初始值,那麼 Hashtable 會直接使用你給定的大小,而 HashMap 會將其擴充為2的冪次方大小(HashMap 中的tableSizeFor()方法保證,下面給出了原始碼)。也就是說 HashMap 總是使用2的冪作為雜湊表的大小,後面會介紹到為什麼是2的冪次方。
  5. 底層資料結構: JDK1.8 以後的 HashMap 在解決雜湊衝突時有了較大的變化,當連結串列長度大於閾值(預設為8)時,將連結串列轉化為紅黑樹,以減少搜尋時間。Hashtable 沒有這樣的機制。

HashSet 和 HashMap 區別

如果你看過 HashSet 原始碼的話就應該知道:HashSet 底層就是基於 HashMap 實現的。(HashSet 的原始碼非常非常少,因為除了 clone() 方法、writeObject()方法、readObject()方法是 HashSet 自己不得不實現之外,其他方法都是直接呼叫 HashMap 中的方法。)

HashSet 和 HashMap 區別

ThoughtWorks准入職Java工程師。專注Java知識分享!開源 Java 學習指南——JavaGuide(12k+ Star)的作者。公眾號多篇文章被各大技術社群轉載。公眾號後臺回覆關鍵字“1”可以領取一份我精選的Java資源哦!

我的公眾號

相關文章