盤:緩存一致性、并發(fā)與JVM核心考點)
2019年7月底我還在實驗室啃《深入理解Java虛擬機》突然收到一個杭州座機打來的電話。接起來才知道是蘑菇街提前批的一面。說實話當(dāng)時有點措手不及因為投完簡歷才三天根本沒想過會這么快好在提前批本身就是臨時起意的機會能走到哪兒是哪兒。后來這一面面了將近一個小時問的內(nèi)容不算偏但每一塊都問得很細尤其是項目細節(jié)和Java基礎(chǔ)答得讓我印象深刻。這幾天陸續(xù)有學(xué)弟學(xué)妹問我當(dāng)年提前批的情況干脆把這場蘑菇街一面完整復(fù)盤出來按面試問題我的答案復(fù)盤點評的結(jié)構(gòu)寫希望能給準(zhǔn)備校招后端崗位的同學(xué)一些參考。1. 提前批的節(jié)奏與我的準(zhǔn)備狀態(tài)1.1 蘑菇街提前批的時間線回顧2019年的秋招比往年更早蘑菇街的提前批大概在7月中旬就開了。我當(dāng)時是通過實驗室學(xué)長內(nèi)推投的投遞的是Java后端開發(fā)崗位。內(nèi)推后的第三天接到面試電話約在當(dāng)天晚上七點電話面試。時間線大概是這個節(jié)奏7月中旬內(nèi)推投遞簡歷投遞后第3天接到一面電話邀約一面電話面試約55分鐘面試形式電話溝通沒有共享屏幕手撕代碼通過口述思路事后發(fā)代碼鏈接完成這里提醒一句蘑菇街當(dāng)時提前批和正式批是不沖突的提前批掛了還可以走正式批。所以不要因為準(zhǔn)備不充分就放棄投遞提前批等于多了一次面試機會拼的就是一個早。1.2 我當(dāng)時的復(fù)習(xí)重心接到面試電話之前我的復(fù)習(xí)已經(jīng)持續(xù)了大概一個月。因為目標(biāo)是后端開發(fā)崗所以復(fù)習(xí)重心分配得很明確刷題劍指Offer全部過了一遍LeetCode按高頻題刷了大概100道重點放在鏈表、二叉樹、動態(tài)規(guī)劃三類。Java基礎(chǔ)HashMap、ArrayList源碼級理解JVM內(nèi)存模型和GC整理成腦圖。并發(fā)編程synchronized、ReentrantLock、volatile、線程池把原理和對比都寫了一遍。數(shù)據(jù)庫MySQL索引底層、事務(wù)隔離級別、MVCC、常見索引失效場景。中間件Redis的數(shù)據(jù)結(jié)構(gòu)、持久化、緩存穿透/擊穿/雪崩。面的過程中我發(fā)現(xiàn)這種按主線復(fù)習(xí)臨時補充的策略是對的。蘑菇街一面并沒有問太偏門的內(nèi)容全部落在Java后端開發(fā)的核心范圍內(nèi)。換句話說只要認真準(zhǔn)備過上述內(nèi)容一面基本都能應(yīng)對。2. 一面全程從自我介紹到項目深挖2.1 開場定調(diào)自我介紹怎么說電話接通后面試官簡單確認了身份直接讓我自我介紹。我的回答大概是這樣面試官您好我是XX大學(xué)軟件工程專業(yè)2020屆研究生研究生期間主要做Java后端開發(fā)方向。對Java基礎(chǔ)、并發(fā)編程、JVM和MySQL底層原理有過系統(tǒng)的學(xué)習(xí)平時用Spring Boot和MyBatis做項目。最近主要做了兩個項目一個是基于微服務(wù)的校園秒殺系統(tǒng)另一個是實驗室的設(shè)備借用管理平臺。其中秒殺系統(tǒng)涉及到高并發(fā)下的庫存扣減和緩存一致性設(shè)計是比較能聊的一個項目。復(fù)盤時回頭看這段自我介紹其實埋了兩個鉤子一是主動把話題引到并發(fā)和緩存二是強調(diào)秒殺項目可以深聊。面試官后續(xù)果然順著項目問了很多這比被動等他隨便問要舒服得多。我的經(jīng)驗是自我介紹不用太長但一定要有意識地引導(dǎo)面試官去問你準(zhǔn)備最充分的部分。2.2 項目深挖秒殺系統(tǒng)的緩存一致性設(shè)計面試官聽完自我介紹沒有問實驗室管理平臺直接問秒殺系統(tǒng)你剛才說秒殺系統(tǒng)涉及高并發(fā)說說最核心的難點你怎么解決的我當(dāng)時把項目里的技術(shù)方案完整講了一遍。這里還原關(guān)鍵對話。面試官庫存扣減怎么設(shè)計的我最開始是直接操作數(shù)據(jù)庫每次下單都update庫存表并且加for update鎖。壓測的時候發(fā)現(xiàn)連接池滿得非常快QPS頂?shù)綆装倬蜕喜蝗チ恕:髞砀某蓛蓪釉O(shè)計——先把庫存預(yù)熱到Redis用Redis的decr命令做庫存扣減扣減成功后再把下單請求丟進RabbitMQ由消費者異步創(chuàng)建訂單。面試官為什么選Redis的decr而不是數(shù)據(jù)庫悲觀鎖我數(shù)據(jù)庫的for update本質(zhì)上是把一行記錄鎖住并發(fā)能力受限于數(shù)據(jù)庫的連接數(shù)和鎖等待時間。而Redis是單線程模型decr命令天然是原子的單機QPS可以支撐到十萬級別比數(shù)據(jù)庫判斷庫存再扣減要快得多。另外這里還有一個細節(jié)用戶維度我們用了Redis setnx做一人一單限制防止同一用戶秒殺多件。面試官緩存和數(shù)據(jù)庫的一致性怎么保證我用的Cache Aside Pattern也就是先更新數(shù)據(jù)庫再刪除緩存。讀到的時候如果緩存miss再從數(shù)據(jù)庫讀出來回填緩存。面試官為什么是刪除緩存而不是更新緩存我更新緩存需要算出最新的數(shù)據(jù)再寫進去成本比刪除高而且并發(fā)場景下兩個線程交替更新緩存和數(shù)據(jù)庫很容易導(dǎo)致緩存里終態(tài)錯誤。刪除緩存成本低即使刪早了下一次讀的時候重新從數(shù)據(jù)庫加載就行邏輯簡單可靠。面試官那刪除緩存失敗怎么辦我我們做了一個兜底緩存key設(shè)置了過期時間就算刪除失敗最終也會過期不會永久不一致。同時我們把刪除失敗的key寫入本地消息表通過RabbitMQ延遲隊列重試刪除。更完善的方案是訂閱MySQL的binlog用canal解析出數(shù)據(jù)變更事件再異步刪除對應(yīng)緩存這樣完全不依賴業(yè)務(wù)代碼手動刪除。面試官對這個回答沒有再追問點了下頭就切到下一題。復(fù)盤下來項目這塊他能問到的點基本都被我提前準(zhǔn)備過。這里最大的心得是項目深挖其實不是考你做得多牛而是考你對方案的理解深度。光說用了Redis不夠得說得出來為什么不用數(shù)據(jù)庫鎖Redis為什么合適刪緩存失敗會有什么問題怎么補救。把這些問題想通了項目環(huán)節(jié)基本穩(wěn)了。2.3 面試官追問的意圖后來想想面試官在每個追問背后其實都在考察一件事項目到底是不是你自己做的你有沒有真正想過方案背后的取舍。比如緩存一致性問題如果只是背過先更新數(shù)據(jù)庫再刪除緩存這句話被問失敗怎么辦就露餡了。所以做項目的時候不能只看博客里怎么寫一定要親手把方案跑通把異常場景都模擬一遍。3. 基礎(chǔ)題問答實錄Java并發(fā)、JVM、MySQL項目聊了大概15分鐘之后面試官話鋒一轉(zhuǎn)進入基礎(chǔ)題環(huán)節(jié)。速度明顯加快基本是我問你答答完就下一題的節(jié)奏。3.1 Java集合與并發(fā)面試官HashMap的底層結(jié)構(gòu)是什么樣的JDK 1.7到1.8有哪些變化我HashMap底層是數(shù)組加鏈表JDK 1.8之后引入了紅黑樹。當(dāng)鏈表長度超過8同時數(shù)組容量達到64鏈表會轉(zhuǎn)為紅黑樹主要是為了處理hash沖突嚴(yán)重時鏈表過長、查詢效率從O(n)惡化的問題。擴容方面默認容量16負載因子0.75也就是元素個數(shù)超過閾值12時就擴容到原來的兩倍。JDK 1.8還有一個重要的優(yōu)化擴容時不用重新計算hash而是看原h(huán)ash值新增的那一位是0還是10就留在原位置1就放到原位置加舊容量的位置。另外1.7是頭插法并發(fā)擴容可能形成環(huán)形鏈表1.8改成尾插法避免了這個問題但HashMap本身線程不安全并發(fā)場景還是要用ConcurrentHashMap。面試官ConcurrentHashMap底層是怎么做線程安全的我1.7的時候是分段鎖內(nèi)部維護多個Segment每個Segment繼承ReentrantLock不同段之間可以并行操作。1.8放棄了分段鎖改用synchronized加CAS鎖的粒度是桶也就是數(shù)組的每個槽位只有發(fā)生hash沖突的桶才會鎖住并發(fā)度更高。擴容時支持多線程協(xié)助遷移把一個大數(shù)組拆成多個任務(wù)分給不同線程去做。面試官volatile和synchronized有什么區(qū)別我volatile只保證可見性和有序性它會強制把修改立即寫回主內(nèi)存同時通過內(nèi)存屏障禁止指令重排序但它不保證原子性。synchronized保證原子性、可見性和有序性。所以像i這種操作光用volatile是不行的。volatile典型的應(yīng)用場景是狀態(tài)標(biāo)志位還有單例模式里的Double Check用volatile修飾instance防止JVM指令重排導(dǎo)致拿到未初始化完成的對象。面試官synchronized和ReentrantLock怎么選我synchronized是JVM層面的鎖ReentrantLock是JDK提供的API。ReentrantLock多了三個能力可以響應(yīng)中斷、可以設(shè)置超時時間、可以創(chuàng)建公平鎖并且支持多個Condition條件隊列。JDK 1.6之后synchronized做了偏向鎖、輕量級鎖的優(yōu)化性能差距已經(jīng)很小。如果只是簡單的同步需求synchronized就夠了代碼更簡潔如果需要超時等待、可中斷、公平性控制就選ReentrantLock。這里有個插曲面試官對公平鎖這個點追問了一句公平鎖的底層怎么實現(xiàn)的我當(dāng)時只說了一個大概ReentrantLock內(nèi)部維護了一個等待隊列公平鎖會檢查隊列里有沒有排在前面的線程有就先讓前面的人獲取鎖。面試官沒再追問。后來我仔細研究了源碼AQS里是通過hasQueuedPredecessors()方法判斷當(dāng)前線程是不是隊列頭部只有真正排在頭部的線程才有資格搶鎖。3.2 JVM內(nèi)存與GC面試官JVM運行時數(shù)據(jù)區(qū)域有哪些哪些線程共享哪些線程私有我整體分五大塊。程序計數(shù)器、虛擬機棧、本地方法棧是線程私有的堆和方法區(qū)是線程共享的。JDK 8之后方法區(qū)被元空間取代元空間使用本地內(nèi)存不再受JVM堆內(nèi)存上限限制。對象實例和數(shù)組主要分配在堆上棧上分配是JIT在逃逸分析之后做的優(yōu)化不是常規(guī)路徑。面試官垃圾回收怎么判斷對象可以回收我主流用的是可達性分析算法。從一組稱為GC Roots的根對象出發(fā)沿著引用鏈往下找沒有被引用鏈連接的對象就判定為可回收。GC Roots包括虛擬機棧中棧幀里的局部變量引用的對象、靜態(tài)變量引用的對象、常量池引用的對象、JNI引用的對象、被synchronized持有的對象。引用計數(shù)法因為無法解決循環(huán)引用的問題現(xiàn)在基本不會單獨用。面試官CMS和G1有什么區(qū)別我CMS是老年代垃圾收集器目標(biāo)是低停頓用的是標(biāo)記-清除算法整個過程分初始標(biāo)記、并發(fā)標(biāo)記、重新標(biāo)記、并發(fā)清除四個階段其中只有初始標(biāo)記和重新標(biāo)記需要STW。缺點也很明顯標(biāo)記-清除會產(chǎn)生內(nèi)存碎片并發(fā)階段會占用CPU資源而且它無法處理浮動垃圾。G1則是把整個堆劃分成多個大小相等的Region既可以回收新生代又可以回收老年代通過維護每個Region的回收價值和回收成本做到可預(yù)測的停頓時間。G1在Java 9之后成為默認垃圾收集器它最大的特點是可以在回收過程中把Region里的存活對象復(fù)制到空閑Region里本質(zhì)上是標(biāo)記-復(fù)制不會產(chǎn)生碎片。JVM這塊我明顯感覺面試官比較滿意因為他在我答完之后說了一句JVM底子還可以。這可能是整場面試中我最舒展的一段。3.3 MySQL索引與事務(wù)面試官InnoDB的索引為什么用B樹我B樹有幾個特點比較適合數(shù)據(jù)庫場景。第一非葉子節(jié)點只存索引鍵值不存數(shù)據(jù)所以每個節(jié)點能存放更多的索引項樹的高度低一般三層就能存上千萬條數(shù)據(jù)也就是最多三次磁盤IO就能定位到葉子節(jié)點。第二葉子節(jié)點之間有雙向指針串聯(lián)天然支持范圍查詢和排序B樹就需要回溯父節(jié)點才能做范圍查詢。第三哈希索引雖然單點查詢快但不支持范圍紅黑樹和二叉樹在數(shù)據(jù)量大的時候樹太高磁盤IO次數(shù)太多了。面試官聯(lián)合索引遵循什么原則哪些情況會導(dǎo)致索引失效我聯(lián)合索引遵循最左前綴原則。比如建了(a, b, c)的聯(lián)合索引查詢條件里有a或者a、b或者a、b、c才能命中。失效場景常見的有對索引列做了計算、函數(shù)操作、隱式類型轉(zhuǎn)換使用like時前面帶百分號比如like %xx使用or連接非索引列聯(lián)合索引中不滿足最左前綴條件。面試官MySQL默認的隔離級別是什么MVCC是怎么實現(xiàn)的我默認是可重復(fù)讀RR。MVCC是InnoDB實現(xiàn)一致性讀的關(guān)鍵機制核心由三部分組成undo log版本鏈、read view、隱藏的trx_id字段。每行記錄上都有最近修改它的事務(wù)IDundo log記錄了歷史版本形成一個版本鏈。查詢時生成read viewread view里保存了活躍事務(wù)列表通過比較事務(wù)ID判斷當(dāng)前查詢能看到哪個版本。區(qū)別在于讀已提交RC是每條語句生成一個新的read view可重復(fù)讀RR是第一次快照讀的時候生成后續(xù)復(fù)用同一個read view所以同一個事務(wù)里兩次查詢結(jié)果一致。面試官RR級別下怎么防止幻讀我主要通過兩個機制。一個是MVCC的快照讀第一次讀的時候生成read view之后復(fù)用即使別的事務(wù)插入了新數(shù)據(jù)當(dāng)前事務(wù)看不到天然避免了幻讀。另一個是當(dāng)前讀比如select ... for update需要通過間隙鎖gap lock和臨鍵鎖next-key lock來實現(xiàn)。間隙鎖鎖的是索引記錄之間的間隙讓其他事務(wù)無法在間隙內(nèi)插入新的記錄從而防止幻讀。基礎(chǔ)題環(huán)節(jié)到這里大概持續(xù)了25分鐘。面試官把Java、JVM、MySQL各自挑了最核心的幾個點來問沒有一上來就壓八股。給我的感覺是蘑菇街一面更看重能不能把原理講清楚而不是背了多少面試題。4. 計算機網(wǎng)絡(luò)與中間件快問快答基礎(chǔ)題之后面試官開始快問快答節(jié)奏明顯加快問題更零散像在掃知識點。4.1 TCP與HTTP細節(jié)面試官TCP三次握手為什么不是兩次我如果只需要兩次握手可能出現(xiàn)這種情況客戶端發(fā)送的SYN報文在網(wǎng)絡(luò)中滯留了很久客戶端認為它超時了沒有收到確認所以重發(fā)了SYN這一次正常完成了連接。但滯留在網(wǎng)絡(luò)中的那個舊SYN報文過了很久又到達了服務(wù)端服務(wù)端以為是一個新連接于是返回SYNACK給客戶端。如果只有兩次握手服務(wù)端這時就認為連接建立成功了會一直等待客戶端發(fā)送數(shù)據(jù)白白浪費服務(wù)端的資源。而三次握手中客戶端收到服務(wù)端的SYNACK之后并不會立即認為連接建立而是要再回一個ACK。如果服務(wù)端收到的是舊SYN的響應(yīng)客戶端會發(fā)現(xiàn)這個連接不是自己期望的就不會回ACK服務(wù)端自然也不會建立連接。面試官四次揮手里TIME_WAIT為什么要等2MSL我兩個原因。第一確保最后一個ACK報文能夠到達對端如果ACK丟了對端會超時重傳FIN如果此時連接已經(jīng)關(guān)閉就沒有辦法重發(fā)ACK了等一個2MSL可以保證ACK重傳的窗口足夠。第二經(jīng)過2MSL的時間能讓本次連接產(chǎn)生的所有舊報文都在網(wǎng)絡(luò)中消失避免它們出現(xiàn)在未來某個相同的四元組連接里造成數(shù)據(jù)混亂。面試官HTTPS的握手過程了解嗎我HTTPS本質(zhì)上是HTTP over TLS握手過程大致分幾步客戶端發(fā)起ClientHello攜帶支持的TLS版本、加密套件列表和隨機數(shù)服務(wù)端返回ServerHello選定加密套件和服務(wù)端隨機數(shù)同時下發(fā)證書客戶端驗證證書合法性然后生成預(yù)主密鑰用服務(wù)端證書里的公鑰加密發(fā)過去服務(wù)端用自己的私鑰解密出預(yù)主密鑰雙方通過三個隨機數(shù)協(xié)商出會話密鑰。之后雙方發(fā)送Finished消息確認握手成功后續(xù)應(yīng)用層數(shù)據(jù)全部走對稱加密。TLS 1.3進一步簡化了握手把以往的兩個往返優(yōu)化成一個往返。面試官HTTP常見的502和504有什么區(qū)別我502 Bad Gateway表示網(wǎng)關(guān)或代理服務(wù)器從上游服務(wù)器收到了無效響應(yīng)簡單說就是上游服務(wù)器掛了或者返回了非法內(nèi)容。504 Gateway Timeout表示網(wǎng)關(guān)在指定時間內(nèi)沒有等到上游服務(wù)器返回響應(yīng)也就是上游處理超時了。實際排查中502更多是后端服務(wù)進程崩潰或者重啟504更多是后端處理得太慢或線程池被打滿。4.2 Redis三大經(jīng)典問題面試官Redis緩存穿透、擊穿、雪崩分別是什么怎么解決我穿透是查詢一個不存在的key緩存里沒有數(shù)據(jù)庫里也沒有請求直接打到數(shù)據(jù)庫惡意攻擊時能把數(shù)據(jù)庫打垮。解決方式是布隆過濾器先用bitmap把所有可能存在的主鍵存進去查不到的直接攔截或者對空結(jié)果也做緩存設(shè)置一個較短的過期時間。擊穿是某一個熱點key在過期瞬間大量請求同時打到數(shù)據(jù)庫。解決方式是熱點key不設(shè)置過期時間或者過期時間加一個隨機值再或者用互斥鎖讓同一個key只有一個請求去數(shù)據(jù)庫回源。雪崩是大量key在同一時間段集體過期導(dǎo)致流量瞬間打到數(shù)據(jù)庫。解決方式有過期時間增加隨機因子避免集中在同一時刻熱點數(shù)據(jù)不設(shè)置過期時間由后臺任務(wù)定時更新還可以做熔斷降級數(shù)據(jù)庫壓力大的時候直接返回默認值。面試官RDB和AOF怎么選我RDB是定時的全量快照文件緊湊恢復(fù)速度快適合做備份和主從同步但故障時可能丟失最后一次快照之后的數(shù)據(jù)。AOF記錄的是每一個寫命令數(shù)據(jù)安全性更高默認everysec配置最多丟一秒數(shù)據(jù)但AOF文件體積更大恢復(fù)速度慢。生產(chǎn)環(huán)境通常兩個都開AOF保證數(shù)據(jù)安全RDB用于快速恢復(fù)和備份。面試官Redis實現(xiàn)分布式鎖要注意什么我最基礎(chǔ)的方式是SETNX加過期時間set key value NX PX 30000保證原子性。但要注意value必須是一個唯一標(biāo)識釋放鎖的時候要先get判斷是不是自己的鎖再delget和del要保證原子性通常用Lua腳本執(zhí)行。更完整的做法是用Redisson它有一個看門狗機制會對鎖自動續(xù)期防止業(yè)務(wù)還沒執(zhí)行完鎖就過期被其他線程拿走了。這套方案里Redis主從切換時可能會丟鎖所以嚴(yán)格要求時要用RedLock但實際業(yè)務(wù)里用得不多。快問快答階段明顯是在掃盲區(qū)問題之間沒有太多關(guān)聯(lián)覆蓋范圍廣但深度不大。我的體感是這部分的目的是快速判斷候選人的知識面夠不夠?qū)挾皇窃谀骋粋€點上死磕。所以平時積累很重要至少每個常見知識點都要能說出個一二三來。5. 手撕算法鏈表中環(huán)的入口節(jié)點基礎(chǔ)題問完面試官說最后寫一道題吧。當(dāng)時電話面試不方便共享屏幕就讓我口述思路然后發(fā)一段代碼到指定的鏈接里。5.1 題目分析與快慢指針?biāo)悸奉}目是經(jīng)典題給定一個鏈表如果它包含環(huán)找出環(huán)的入口節(jié)點沒有環(huán)就返回null。我聽到題目第一反應(yīng)是這題有套路分兩步第一步判斷是否有環(huán)。用快慢指針slow每次走一步fast每次走兩步兩個指針都從頭節(jié)點出發(fā)。如果鏈表中存在環(huán)那么快指針最終會追上慢指針在環(huán)內(nèi)相遇如果快指針達到了鏈表尾部說明沒有環(huán)。第二步找到環(huán)的入口。相遇之后讓slow回到頭節(jié)點fast留在相遇點然后兩個指針都保持每次走一步的速度繼續(xù)走下一次相遇的節(jié)點就是環(huán)的入口。但光記住結(jié)論不夠面試官多半會追問為什么。所以當(dāng)時我把推導(dǎo)也講了一遍假設(shè)從頭節(jié)點到環(huán)入口的距離是a環(huán)入口到第一次相遇點的距離是b相遇點到環(huán)入口的距離是c那么第一次相遇時慢指針走了ab快指針走了abn(bc)。因為快指針?biāo)俣仁锹羔樀膬杀端?(ab)abn(bc)整理一下得到a (n-1)(bc)c。也就是說從頭節(jié)點重新出發(fā)的slow指針走距離a的同時從相遇點重新出發(fā)的fast指針會繞環(huán)走n-1圈再走c兩者恰好都在環(huán)入口位置碰頭。5.2 代碼實現(xiàn)與邊界條件我用Java快速寫出了實現(xiàn)public class ListNode { int val; ListNode next; ListNode(int x) { val x; } } public class Solution { public ListNode detectCycle(ListNode head) { if (head null || head.next null) { return null; } ListNode slow head; ListNode fast head; while (fast ! null fast.next ! null) { slow slow.next; fast fast.next.next; if (slow fast) { break; } } if (fast null || fast.next null) { return null; } slow head; while (slow ! fast) { slow slow.next; fast fast.next; } return slow; } }寫完之后我主動把邊界條件說了一遍空鏈表和只有一個節(jié)點的情況直接返回null。鏈表沒有環(huán)fast會先到尾部循環(huán)自然結(jié)束。鏈表整個就是一個環(huán)也就是尾節(jié)點指向頭節(jié)點slow回到頭節(jié)點后第一次判斷就與fast相等直接返回頭節(jié)點邏輯是成立的。環(huán)在鏈表中間慢指針回到頭節(jié)點后走a步到入口另一個從相遇點出發(fā)經(jīng)過c步到達入口兩者同步到達。5.3 面試官加試為什么快指針每次走兩步果然面試官接著問為什么快指針每次走兩步走三步行不行我心里慶幸之前看過這個問題的分析。回答思路是快指針走兩步的目的是保證它在慢指針進入環(huán)之后一定能追上慢指針。考慮慢指針剛進入環(huán)的時候快指針已經(jīng)在環(huán)里了兩個指針之間的相對距離最多是環(huán)長減1。每次快指針比慢指針多走一步這個相對距離就會縮小1所以最多走環(huán)長減1步就一定能追上時間復(fù)雜度是O(n)是最優(yōu)步長。如果快指針走三步相對距離每次縮小2那么當(dāng)初始相對距離是偶數(shù)時能追上是奇數(shù)時就可能錯過。極端情況下快指針會一直跳過慢指針?biāo)诘奈恢迷斐捎啦慌雒娴那闆r。當(dāng)然實際中可能會因為環(huán)的形狀不同而碰巧追上但無法保證一定有解。所以兩步是既能保證追上又不會浪費額外時間的穩(wěn)妥選擇。算法題到這里結(jié)束。面試官簡單評價了一句思路清晰然后問我有沒有想問他的問題。這里我也說一個小經(jīng)驗算法題不光要把代碼寫出來最好能主動說明復(fù)雜度和邊界條件。很多面試官不會明確要求你說但你說了他會下意識把你歸類到基礎(chǔ)扎實的那一批。這道題的時間復(fù)雜度是O(n)空間復(fù)雜度是O(1)我也一并說了。6. 面后復(fù)盤與經(jīng)驗總結(jié)6.1 我回答得不夠好的地方這一面整體感覺不差但復(fù)盤時我給自己挑出了幾個明顯的問題第一線程池參數(shù)當(dāng)時答得不全。面試官問線程池的核心參數(shù)有哪些我答了corePoolSize和maxPoolSize但把keepAliveTime、workQueue、ThreadFactory這幾項漏了還是面試官引導(dǎo)了一下才補齊。這個屬于基礎(chǔ)中的基礎(chǔ)答不全挺不應(yīng)該的。后來我把ThreadPoolExecutor的七個參數(shù)核心線程數(shù)、最大線程數(shù)、空閑存活時間、時間單位、工作隊列、線程工廠、拒絕策略整理成了一張表每天默寫一遍之后再沒有出現(xiàn)過卡頓。第二反問環(huán)節(jié)問得太淺。我只問了一句團隊目前主要用什么技術(shù)棧面試官簡單回答完就結(jié)束了。后來跟已經(jīng)拿到offer的學(xué)長聊才知道好的反問是能加分的。比如可以問團隊目前更多在攻堅哪塊業(yè)務(wù)候選人入職后一般從什么模塊入手您覺得這個崗位更看重候選人的哪方面能力這能體現(xiàn)你對崗位的思考深度。當(dāng)然這一面本身已經(jīng)過去了這個經(jīng)驗主要是在后面的面試?yán)镉蒙狭恕5谌椖坷镉幸粋€細節(jié)我沒講透。面試官問過為什么訂單創(chuàng)建不直接同步執(zhí)行而是走MQ異步我當(dāng)時只說為了削峰填谷但沒有說清楚異步之后怎么保證訂單和庫存的一致性。實際上我們當(dāng)時的方案是MQ消費者里做庫存二次校驗如果庫存扣減成功但訂單創(chuàng)建失敗會發(fā)一條消息到死信隊列由定時任務(wù)做狀態(tài)對賬。如果當(dāng)時把這個對賬機制講出來項目這塊會更加完整。6.2 蘑菇街一面的考察特點如果把蘑菇街一面和同期面過的其他公司對比我的感受是技術(shù)范圍中規(guī)中矩不偏門。核心還是Java基礎(chǔ)、JVM、MySQL、Redis、算法這些后端通用知識。項目深挖比想象中要細。會在緩存一致性、超賣怎么解決這類問題上連續(xù)追問直到確定你是真的理解而不是背了個八股。算法題難度適中。沒有出hard題劍指Offer和LeetCode hot 100覆蓋到的程度就夠用了。面試官整體比較溫和會有一點引導(dǎo)性。你卡住的時候他會換個角度問而不是冷場讓你尷尬。這里也列一個表格方便大家對照我當(dāng)時整理的考察側(cè)重點考察模塊涉及知識點準(zhǔn)備優(yōu)先級項目深挖緩存一致性、超賣、異步解耦、消息可靠性最高Java基礎(chǔ)HashMap、ConcurrentHashMap、volatile、鎖高JVM內(nèi)存區(qū)域、GC Roots、CMS/G1高MySQLB樹、索引失效、MVCC、間隙鎖高計算機網(wǎng)絡(luò)TCP握手揮手、HTTPS、HTTP狀態(tài)碼中中間件Redis穿透/擊穿/雪崩、分布式鎖中算法鏈表、二叉樹、雙指針高6.3 寫在最后的心得這場面試最終的結(jié)果是過了后續(xù)進入了二面。但說實話一面給我留下的最深印象不在結(jié)果而在于它讓我第一次真正體會到準(zhǔn)備充分的面試是很有掌控感的。項目、基礎(chǔ)題、算法三個環(huán)節(jié)節(jié)奏分明面試官問的每個深度點我都恰好提前踩過這種正反饋是會滾雪球的最直接的影響就是讓我對后續(xù)字節(jié)、快手幾家大廠的面試都更有底氣。如果你現(xiàn)在也在準(zhǔn)備秋招我的建議就是提前批一定要投尤其像蘑菇街這種有提前批的廠子試一試沒有任何損失。復(fù)習(xí)時間不夠的時候優(yōu)先把項目里每一個技術(shù)選型的前因后果想明白把HashMap、JVM、MySQL索引、Redis三大問題這幾座大山啃透再拿劍指Offer里的高頻題練手。面經(jīng)不是背的是拿來對照檢查自己哪里有漏洞的用這個思路去看你會少走很多彎路。