美團(tuán)面試,問的都是基礎(chǔ)?。?/h1>
大家好,我是小林。
今天分享一位讀者的春招面經(jīng),美團(tuán)基礎(chǔ)架構(gòu)的面經(jīng)。
問的全是基礎(chǔ),一個編程語言的問都沒有。
問題記錄
MySQL-MVCC
讀者答:InooDB是通過 MVCC 實現(xiàn)可重復(fù)讀的隔離級別的,MVCC 就是多版本并發(fā)控制,它其實記錄了歷史版本的數(shù)據(jù),解決了讀寫并發(fā)沖突問題。有一個版本編碼,然后它進(jìn)入了各種操作下的數(shù)據(jù)狀態(tài),能夠根據(jù)當(dāng)前這個指令的狀態(tài)來讀取不同時期的數(shù)據(jù)快照。主要實現(xiàn)方法的話就是通過事務(wù)版本號,讀取視圖還有undo日志進(jìn)行完善的。
小林補(bǔ)充:具體的實現(xiàn)原理過程,可以去 xiaolincoding.com 網(wǎng)站->圖解MySQL->事務(wù)隔離級別是怎么實現(xiàn)的?這篇文章學(xué)習(xí)。
MySQL-原子性怎么實現(xiàn)的
說錯了,說成redoLog了。應(yīng)該是undoLog。
讀者答:原子性的話會在寫數(shù)據(jù)之前有一個,就是WAL的過程,就是寫一個 redo log,然后如果數(shù)據(jù)沒有寫完或者是執(zhí)行操作失敗的話,可以恢復(fù)所有已提交的事務(wù)或者回滾。
小林補(bǔ)充:
事務(wù)的原子性是通過 undo log 實現(xiàn)的。
undo log 是一種用于撤銷回退的日志。在事務(wù)沒提交之前,MySQL 會先記錄更新前的數(shù)據(jù)到 undo log 日志文件里面,當(dāng)事務(wù)回滾時,可以利用 undo log 來進(jìn)行回滾。如下圖:
回滾事務(wù)
每當(dāng) InnoDB 引擎對一條記錄進(jìn)行操作(修改、刪除、新增)時,要把回滾時需要的信息都記錄到 undo log 里,比如:
- 在插入一條記錄時,要把這條記錄的主鍵值記下來,這樣之后回滾時只需要把這個主鍵值對應(yīng)的記錄刪掉就好了;
- 在刪除一條記錄時,要把這條記錄中的內(nèi)容都記下來,這樣之后回滾時再把由這些內(nèi)容組成的記錄插入到表中就好了;
- 在更新一條記錄時,要把被更新的列的舊值記下來,這樣之后回滾時再把這些列更新為舊值就好了。
在發(fā)生回滾時,就讀取 undo log 里的數(shù)據(jù),然后做原先相反操作。比如當(dāng) delete 一條記錄時,undo log 中會把記錄中的內(nèi)容都記下來,然后執(zhí)行回滾操作的時候,就讀取 undo log 里的數(shù)據(jù),然后進(jìn)行 insert 操作。
不同的操作,需要記錄的內(nèi)容也是不同的,所以不同類型的操作(修改、刪除、新增)產(chǎn)生的 undo log 的格式也是不同的,具體的每一個操作的 undo log 的格式我就不詳細(xì)介紹了,感興趣的可以自己去查查。
MySQL-持久性是怎么實現(xiàn)的
讀者答:通過 redo log 保證持久化。buffer pool 中有 undo 頁,對 undo 頁的修改也都會記錄到 redo log。redo log 會每秒刷盤,提交事務(wù)時也會刷盤,數(shù)據(jù)頁和 undo 頁都是靠這個機(jī)制保證持久化的。
通過兩次寫來實現(xiàn),當(dāng)緩沖池的臟頁刷新時,并不直接寫磁盤,而是會通過memcpy函數(shù)將臟頁先拷貝到內(nèi)存中的doublewrite buffer,之后通過doublewrite buffer再分兩次,每次寫入1MB到共享表空間的物理磁盤上,然后馬上調(diào)用fsync函數(shù),同步磁盤,進(jìn)行數(shù)據(jù)持久化。
小林補(bǔ)充:
事務(wù)的持久性是通過 redo log 實現(xiàn)的。
我們修改某條記錄,其實該記錄并不是馬上刷入磁盤的,而是將 Innodb 的 Buffer Pool 標(biāo)記為臟頁,等待后續(xù)的異步刷盤。
Buffer Pool 是提高了讀寫效率沒錯,但是問題來了,Buffer Pool 是基于內(nèi)存的,而內(nèi)存總是不可靠,萬一斷電重啟,還沒來得及落盤的臟頁數(shù)據(jù)就會丟失。
為了防止斷電導(dǎo)致數(shù)據(jù)丟失的問題,當(dāng)有一條記錄需要更新的時候,InnoDB 引擎就會先更新內(nèi)存(同時標(biāo)記為臟頁),然后將本次對這個頁的修改以 redo log 的形式記錄下來,這個時候更新就算完成了。
后續(xù),InnoDB 引擎會在適當(dāng)?shù)臅r候,由后臺線程將緩存在 Buffer Pool 的臟頁刷新到磁盤里,這就是 WAL (Write-Ahead Logging)技術(shù)。
WAL 技術(shù)指的是, MySQL 的寫操作并不是立刻寫到磁盤上,而是先寫日志,然后在合適的時間再寫到磁盤上。
過程如下圖:
redo log 是物理日志,記錄了某個數(shù)據(jù)頁做了什么修改,比如對 XXX 表空間中的 YYY 數(shù)據(jù)頁 ZZZ 偏移量的地方做了AAA 更新,每當(dāng)執(zhí)行一個事務(wù)就會產(chǎn)生這樣的一條或者多條物理日志。
在事務(wù)提交時,只要先將 redo log 持久化到磁盤即可,可以不需要等到將緩存在 Buffer Pool 里的臟頁數(shù)據(jù)持久化到磁盤。
當(dāng)系統(tǒng)崩潰時,雖然臟頁數(shù)據(jù)沒有持久化,但是 redo log 已經(jīng)持久化,接著 MySQL 重啟后,可以根據(jù) redo log 的內(nèi)容,將所有數(shù)據(jù)恢復(fù)到最新的狀態(tài)。
操作系統(tǒng)-死鎖怎么產(chǎn)生的
讀者答:死鎖會產(chǎn)生的話一般會出現(xiàn)就是嗯資源就是互相占用,但是沒有辦法解鎖,形成循環(huán)這樣的情況,比如說 a 線程有一部分 b 線程需要的資源, b 線程有一部分 a 需要的資源,那他兩個人互相的互斥等待形成了死鎖,兩個線程都沒有辦法完成任務(wù)。
小林補(bǔ)充:
死鎖問題的產(chǎn)生是由兩個或者以上線程并行執(zhí)行的時候,爭奪資源而互相等待造成的。
死鎖只有同時滿足互斥、持有并等待、不可剝奪、環(huán)路等待這四個條件的時候才會發(fā)生。
所以要避免死鎖問題,就是要破壞其中一個條件即可,最常用的方法就是使用資源有序分配法來破壞環(huán)路等待條件。
操作系統(tǒng)-怎么避免死鎖
讀者答:
- 避免死鎖的話可以手動的 kill 掉某一個進(jìn)程來結(jié)束當(dāng)前的死鎖狀態(tài)。
- 也可以說設(shè)置一些搶占的規(guī)則。如果這個進(jìn)程占用的時間非常長的話,通過上下文切換給另外一個進(jìn)程運(yùn)行的機(jī)會,
- 也可以在分配資源的時候進(jìn)行預(yù)先的設(shè)計,就是有一個銀行家算法來進(jìn)行一個不會發(fā)生死鎖的進(jìn)程運(yùn)行的調(diào)度
操作系統(tǒng)-pageCache是什么
讀者答:緩存一些比較常訪問的文件到緩存中,這樣子的話它就能減少兩次從內(nèi)核空間拷貝的過程,就是來減少查詢這個內(nèi)容的時間。
小林補(bǔ)充:
為了提升對文件的讀寫效率,Linux 內(nèi)核會以頁大?。?KB)為單位,將文件劃分為多數(shù)據(jù)塊。當(dāng)用戶對文件中的某個數(shù)據(jù)塊進(jìn)行讀寫操作時,內(nèi)核首先會申請一個內(nèi)存頁(稱為 頁緩存)與文件中的數(shù)據(jù)塊進(jìn)行綁定。如下圖所示:
如上圖所示,當(dāng)用戶對文件進(jìn)行讀寫時,實際上是對文件的 頁緩存 進(jìn)行讀寫。所以對文件進(jìn)行讀寫操作時,會分以下兩種情況進(jìn)行處理:
- 當(dāng)從文件中讀取數(shù)據(jù)時,如果要讀取的數(shù)據(jù)所在的頁緩存已經(jīng)存在,那么就直接把頁緩存的數(shù)據(jù)拷貝給用戶即可。否則,內(nèi)核首先會申請一個空閑的內(nèi)存頁(頁緩存),然后從文件中讀取數(shù)據(jù)到頁緩存,并且把頁緩存的數(shù)據(jù)拷貝給用戶。
- 當(dāng)向文件中寫入數(shù)據(jù)時,如果要寫入的數(shù)據(jù)所在的頁緩存已經(jīng)存在,那么直接把新數(shù)據(jù)寫入到頁緩存即可。否則,內(nèi)核首先會申請一個空閑的內(nèi)存頁(頁緩存),然后從文件中讀取數(shù)據(jù)到頁緩存,并且把新數(shù)據(jù)寫入到頁緩存中。對于被修改的頁緩存,內(nèi)核會定時把這些頁緩存刷新到文件中。
計算機(jī)網(wǎng)絡(luò)-TCP的可靠性和順序性怎么實現(xiàn)的
讀者答:TCP 它實現(xiàn)可靠性和有序性的操作的話,是通過快重傳或者是回退 n 這樣子的設(shè)計來實現(xiàn)。如果報文在傳遞的過程中丟失之后能夠進(jìn)行重傳。而會怎么能發(fā)現(xiàn)這個報文丟失呢?主要是根據(jù)一些序列號和 ACK的配合來幫助兩個服務(wù)之間知道當(dāng)前傳遞的信息會丟失。
計算機(jī)網(wǎng)絡(luò)-怎么進(jìn)行流量控制的?
回答成擁塞控制了;
讀者答:內(nèi)部維護(hù)了一個能接收消息的一個窗口的大小,如果他出現(xiàn)就是消息丟失的情況,然后這個消息窗口的大小會減半。啟動的時候采用慢啟動的方式,從0開始指數(shù)級增加窗口大小,直到到達(dá)閥值之后線性增加窗口大小。
小林補(bǔ)充:
流量控制主要是可以讓「發(fā)送方」根據(jù)「接收方」的實際接收能力控制發(fā)送的數(shù)據(jù)量。
實現(xiàn)的方式,接收方會有一個接收緩沖區(qū),如果內(nèi)核接收到了數(shù)據(jù),沒有被應(yīng)用讀取的話,接收窗口就會收縮,然后會在tcp報文攜帶接收窗口的大小,發(fā)送發(fā)收到后,就會控制的發(fā)送流量。
下面舉個栗子,為了簡單起見,假設(shè)以下場景:
- 客戶端是接收方,服務(wù)端是發(fā)送方
- 假設(shè)接收窗口和發(fā)送窗口相同,都為 200
- 假設(shè)兩個設(shè)備在整個傳輸過程中都保持相同的窗口大小,不受外界影響
流量控制
根據(jù)上圖的流量控制,說明下每個過程:
- 客戶端向服務(wù)端發(fā)送請求數(shù)據(jù)報文。這里要說明下,本次例子是把服務(wù)端作為發(fā)送方,所以沒有畫出服務(wù)端的接收窗口。
- 服務(wù)端收到請求報文后,發(fā)送確認(rèn)報文和 80 字節(jié)的數(shù)據(jù),于是可用窗口 Usable 減少為 120 字節(jié),同時 SND.NXT 指針也向右偏移 80 字節(jié)后,指向 321,這意味著下次發(fā)送數(shù)據(jù)的時候,序列號是 321。
- 客戶端收到 80 字節(jié)數(shù)據(jù)后,于是接收窗口往右移動 80 字節(jié),RCV.NXT 也就指向 321,這意味著客戶端期望的下一個報文的序列號是 321,接著發(fā)送確認(rèn)報文給服務(wù)端。
- 服務(wù)端再次發(fā)送了 120 字節(jié)數(shù)據(jù),于是可用窗口耗盡為 0,服務(wù)端無法再繼續(xù)發(fā)送數(shù)據(jù)。
- 客戶端收到 120 字節(jié)的數(shù)據(jù)后,于是接收窗口往右移動 120 字節(jié),RCV.NXT 也就指向 441,接著發(fā)送確認(rèn)報文給服務(wù)端。
- 服務(wù)端收到對 80 字節(jié)數(shù)據(jù)的確認(rèn)報文后,SND.UNA 指針往右偏移后指向 321,于是可用窗口 Usable 增大到 80。
- 服務(wù)端收到對 120 字節(jié)數(shù)據(jù)的確認(rèn)報文后,SND.UNA 指針往右偏移后指向 441,于是可用窗口 Usable 增大到 200。
- 服務(wù)端可以繼續(xù)發(fā)送了,于是發(fā)送了 160 字節(jié)的數(shù)據(jù)后,SND.NXT 指向 601,于是可用窗口 Usable 減少到 40。
- 客戶端收到 160 字節(jié)后,接收窗口往右移動了 160 字節(jié),RCV.NXT 也就是指向了 601,接著發(fā)送確認(rèn)報文給服務(wù)端。
- 服務(wù)端收到對 160 字節(jié)數(shù)據(jù)的確認(rèn)報文后,發(fā)送窗口往右移動了 160 字節(jié),于是 SND.UNA 指針偏移了 160 后指向 601,可用窗口 Usable 也就增大至了 200。
Redis-怎么持久化的數(shù)據(jù)
讀者答:Redis 的話,它其實提供了兩種持久化數(shù)據(jù)的方法,一種是AOF,一種是RDB。然后 AOF 的話它是一種,就是說每一條操作信息它都會進(jìn)行追加記錄這樣的一種持久化的方式。當(dāng)那個數(shù)據(jù)庫重新啟動的時候,它就會根據(jù) AOF 里面記錄的數(shù)據(jù)操作,然后來進(jìn)行一個數(shù)據(jù)庫內(nèi)容的重建。而 RDB 的話,它是做快照,也就是說在數(shù)據(jù)庫運(yùn)行的過程中,它可能會另開一個 IO 的線程來進(jìn)行數(shù)據(jù)庫的快照記錄,這樣子的話來記錄它某一個時間段的數(shù)據(jù)情況,這樣子它進(jìn)行恢復(fù),數(shù)據(jù)庫再次啟動的時候就可以直接根據(jù) RDB 文件來進(jìn)行恢復(fù)這兩個操作。
這樣一執(zhí)行的話就可以看出來, AOF 的話,它雖然就是在執(zhí)行的過程中性能的損耗是小的,但是如果數(shù)據(jù)庫要進(jìn)行重新啟動的話,那它需要的耗時是比較長的。而 RDB 的話,它雖然重新啟動的耗時小,但是說它在過程中會有一定的性能損耗。而且如果是在兩個快照創(chuàng)建的中間就是數(shù)據(jù)庫宕機(jī),或者是這樣子沒有做成快照的話,會造成一部分?jǐn)?shù)據(jù)的缺失。
Redis-集群是怎么做的?就是數(shù)據(jù)怎么分片的,然后它的集群的高可用是什么?怎么部署的,這個有沒有了解過?
讀者答:我了解到它是有一個主從模型的,從它從模型的話就是復(fù)制一份主節(jié)點的備份,然后如果主節(jié)點宕機(jī)的情況下,從節(jié)點是可以成為主節(jié)點來提供服務(wù)的,別的就沒有什么了解的。
后續(xù)查資料補(bǔ)充add:在應(yīng)對數(shù)據(jù)量擴(kuò)容時,雖然增加內(nèi)存這種縱向擴(kuò)展的方法簡單直接,但是會造成數(shù)據(jù)庫的內(nèi)存過大,導(dǎo)致性能變慢。Redis 切片集群提供了橫向擴(kuò)展的模式,也就是使用多個實例,并給每個實例配置一定數(shù)量的哈希槽,數(shù)據(jù)可以通過鍵的哈希值映射到哈希槽,再通過哈希槽分散保存到不同的實例上。這樣做的好處是擴(kuò)展性好,不管有多少數(shù)據(jù),切片集群都能應(yīng)對。
分布式-分布式事務(wù)是什么
讀者答:我只知道分布式事務(wù)中的 2 階段提交和 3 階段提交這樣一個概念
- 2 階段提交:準(zhǔn)備階段和提交階段
- 3 階段提交:比2PC多了一個階段,即把原來2PC的準(zhǔn)備階段拆分成CanCommit和PreCommit兩個階段,同時引入超時機(jī)制來解決2PC的同步阻塞問題。
后續(xù)查資料補(bǔ)充add:實際使用都是使用消息隊列+本地消息表保證最終一致性,2PC這種強(qiáng)一致性用在一些金融業(yè)務(wù)中,實現(xiàn)很麻煩。
分布式-paxos和raft的區(qū)別
讀者答:Paxos在一個節(jié)點當(dāng)選為就是 leader 節(jié)點之后,其他的從節(jié)點如果不滿主節(jié)點的那個投票策略的話,是可以對主節(jié)點的投票就是進(jìn)行否決的。Paxos就是三階段提交。但是 raft 的話就是只要集群中存在 leader 節(jié)點的話,從節(jié)點就是會按照主節(jié)點的策略來進(jìn)行一致性的執(zhí)行。
分布式-為什么就是分布式的共識算法都需要要求多數(shù)派提交才能完成它的分布式一致性?
讀者答:有作惡節(jié)點,消息可能到的順序不一樣,扯了拜占庭問題
場景題
- 設(shè)計一個線程池
- LRU 緩存設(shè)計
面試總結(jié)
感覺
面試官想多要些人填志愿,基礎(chǔ)知識沒有深挖,所有的知識點都考察了一下
不足之處
- 基礎(chǔ)知識還要再扎實
- 場景題需要結(jié)合實際開發(fā),有些過于書本知識了,實際不用