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