程式設計師小扎-位元跳動公司的二面

語言: CN / TW / HK

在上週通過著名公司【位元跳動】的一面之後,程式設計師小扎迎來了至關重要的二面,很往常一樣,今天小扎的身體又不舒服了,按照慣例和領導請了半天假。由於早上要去現場面試了,早早起床的小扎快速的完成了洗漱,穿上格子襯衣,背個電腦包,騎上共享單車,提前半小時來到了位元跳動公司了。“你好,我是小扎,我是來面試的”,程式設計師小扎和美麗的前臺小姐姐說道,“好的,稍等,面試官還沒到,我先領你到這邊會議室,你先等會”。

在等了大概半小時後,一位大腹便便的老面試官走了進來,這時他們面面相覷,雙方都笑了,因為他們撞衫了,在尷尬之後,雙方點頭示意,老面試官一本正經的端坐在椅子上,看著小扎的簡歷,心想:至今10個人有9個跪在我這裡,況且如果讓你過了,以後我這個典藏的格子襯衣還能穿嗎,今天得放大招了。小扎萬萬沒想到今天因為一件格子襯衣,讓他陷入一場苦戰。

Redis篇

「老面試官」:如果要實現一個排行的功能,你有什麼想法?

「小扎」:可以用Redis的zset,把要參與排序的權重作為分值,設定到zset中。

「老面試官」:那如果我要知道小明的排名,該用什麼命令呢?

「小扎」:(這老東西,竟然問這麼細),用 zrank key 小明 即可獲得小明的排名。

「老面試官」:那zset的底層是什麼資料結構?

「小扎」:用的跳躍表。

「老面試官」:那你給我介紹下跳躍表的好處是什麼?

「小扎」:(。。。,看來這是個麵霸),好的,我們先來看看沒有跳躍表的時候會怎麼樣:

比如現在如果我要找 7 這個數字,那麼由於zset是順序的,只能重頭開始一個一個找,要找到最後(1->2->3->4->5->6->7),很顯然,速度是挺慢的,但是如果現在我給1、4、7分別再加一層

那麼此時找7,只需要1->4->7三步即可,大大減少了時長,這就是跳躍表的好處。

「老面試官」:那Redis的zet除了使用跳躍表還有使用其他的資料結構嗎?

img

「小扎」:(這是要打破砂鍋問到底呀~,今天我是和你剛上了),還有hash表,hash表記錄了value和score的對映,比如我要取某個成員的分值時,通過hash可以達到O(1)的時間複雜度。

「老面試官」:這樣啊,那還有其他資料結構嗎?

「小扎」:嗯~(竟然還有其他的,這是要敗了呀!等等,我好像想到了什麼),我記得好像某些情況下,zset會使用壓縮列表,比如在鍵值對數量少於128個且每個元素的長度小於64位元組的時候,zset會使用壓縮列表來實現。

「老面試官」:那你能解釋下為什麼這時候使用壓縮列表嗎?

「小扎」:是這樣的,首先我們知道如果使用跳躍表來實現可以發現因為有不同的層高,因此需要更多的儲存空間,但是在資料量不多的情況下,就算重頭開始遍歷應該也是很快的,但是消耗的記憶體空間可以更小,這是速度與儲存之間的綜合考慮吧。

老面試官陷入沉思(這小子不錯嘛,這個知識點沒啥問的了,再深入我也不會了)。

「老面試官」:我們知道Redis很快,但是你知道哪些操作會導致Redis慢嗎?

「小扎」:(這還跟我拐彎抹角呢),你說的是什麼會導致Redis阻塞是吧~

「老面試官」:可以這樣理解。

「小扎」:首先不要使用keys *命令,這個命令會導致Redis掃描所有的key,那麼在沒有掃描完成之前,其他的命令會被阻塞。

「老面試官」:那要如何避免這種情況呢?

「小扎」:生產環境一般建議禁止使用這個命令,同時如果要模糊查詢某些key,可以通過scan掃描法來檢索。

「老面試官」:嗯嗯,那還有其他導致阻塞的原因嗎?

「小扎」:有的,操作大key,當我們寫入一個大key的時候,會導致redis需要分配更多的記憶體,這個過程相對耗時,刪除一個大key的時候,釋放更多的空間也相對耗時。

「老面試官」:不好意思,我打斷一下,你說的刪除大key會阻塞是吧,那有什麼方法刪除不阻塞嗎?

「小扎」:(強顏歡笑),在Redis4.0版本以後支援,大key支援非同步刪除,這樣可以不阻塞主執行緒。

「老面試官」:你繼續。

「小扎」:好的,當使用一些複雜的指令時可能也會造成阻塞,比如Redis裡有個型別叫集合,如果要取多個集合的並集有個指令叫做 sunion ,當我們sunion多個集合的時候,除了單純的合併資料以外,Redis還會去重,這個操作不僅耗費記憶體還會相對耗費CPU,所以可能會阻塞,尤其在資料很多的時候。還有比如AOF持久化使用的是 always 方案,即每次都刷盤,在QPS較高的情況下,可能會造成阻塞。

「老面試官」:等等,我再打斷下,你說的always能具體再說說嗎,平時開啟AOF,難道不是直接寫磁碟嗎?

「小扎」:正常情況下,當我們寫一個數據到檔案的時候,其實是寫入的檔案系統的緩衝區的,並沒有真正的刷到磁碟上,因為磁碟是緩慢的,作業系統為了提升效能,會在緩衝區快滿的時候或者定期執行刷盤操作,這樣做的好處可以節省多次磁碟IO,但是有丟失資料的風險,Redis的AOF設定成always的意思就是,每次寫AOF的時候直接將緩衝區的資料刷入磁碟中。

「老面試官」:(解釋的還行),那你繼續。

「小扎」:(打斷這麼多次,我講到哪了~),哦,如果記憶體快滿了,這時候新的寫入可能會導致Redis進行記憶體淘汰,這個過程可能會相對慢些,大概就這麼多

「老面試官」:(還行吧,不會是背的八股文吧,我再來引導看看他的反應如何)嗯~,你知道Redis是如何刪除過期key的嗎?

「小扎」:這個知道,一共分為兩種,一種是惰性刪除,一種是定時刪除,惰性刪除就是我們在讀某個key的時候,如果發現已經過期了,那麼就順帶刪除。定時刪除是Redis內部有個定時程式,每隔一段時間會隨機檢索一批key,檢查他們是否過期,過期的話,就刪除。

「老面試官」:那如果在某一瞬間,大量的請求進來,並且每個請求的key是不同的,同時每個key都是過期的,這時會出現什麼現象。

「小扎」:哦哦,會觸發惰性刪除,那麼就會導致Redis去釋放記憶體,可能會造成阻塞。這也是造成Redis可能阻塞的一個原因。(尷尬,竟然沒想到這點,這下他耀武揚威了)。

「老面試官」:(還不錯,能想到這個點,不過我背的面試題也就這麼多,換個話題吧)

img

Golang篇

老面試官再次看了看簡歷,說到:原來你是面試go開發的。

「小扎」:(我xx,現在才發現)是的。

「老面試官」:那我來問問go相關的知識吧,先說說map是執行緒安全的嗎?

「小扎」:不是。

「老面試官」:那如何解決map併發讀寫的問題呢?

「小扎」:可以加鎖,比如 sync.Mutex

「老面試官」:加鎖效能會不會很差?

「小扎」:有點,如果是讀多寫少的情況下,可以使用 sync.map

「老面試官」:sync.map為什麼適合讀多寫少的情況?

「小扎」:sync.map其實是用的是空間換時間的思想,它內部其實有兩個map,一個是叫read,只提供讀的,存的資料是atomic.Value型別,因此讀是執行緒安全的,在大量讀的情況下,是不需要加鎖的,還有個叫dirty的map,新資料的寫入是進dirty的,當我們read map中沒讀到資料的時候會去dirty中讀,這個過程是要加鎖的,同時在read miss一定次數後,就會把dirty的資料複製給read,這樣read的資料就是最新的了,綜合來看相比常規的map無腦加鎖,sync.map的讀大部分場景還是不需要加鎖的,因此在讀多寫少的情況下,效能還是不錯的。

「老面試官」:那slice是執行緒安全的嗎?

「小扎」:slice也不是執行緒安全的,但是在發生併發的時候,slice不會panic,會存在資料覆蓋的問題。

「老面試官」:slice和陣列是什麼關係?

「小扎」:slice的底層就是引用的陣列。

「老面試官」:陣列之間是能比較的嗎?

「小扎」:同類型的陣列是可以比較的,比如[2]int{1,2}和[2]int{2,3},但是如果是[2]int{1,2}和[3]int{1,2,3}是不能比較的,此時連編譯都不能通過。

「老面試官」:那slice之間是可以比較的嗎?

「小扎」:slice之間是不能比較的,slice只能和nil比較。

「老面試官」:那如果兩個都是nil的slice呢?比如:

var a []int
var b []int
fmt.Println(a == b)

「小扎」:也是不行的,正如前面說的,slice只能和nil比較。 「老面試官」 :chan有哪些型別?並說說他們的區別 「小扎」 :帶緩衝的和不帶緩衝的,對於不帶緩衝的chan來說,應用場景就是阻塞的,對於帶緩衝的chan來說,在緩衝沒有滿的時候可以不阻塞,滿了之後就阻塞了。

「老面試官」:給一個已經close的chan發資料會發生什麼?

「小扎」:會panic。

「老面試官」:那從一個已經close的chan取資料會發生什麼?

「小扎」:會得到chan型別的零值,比如int型別的chan就會拿到0。

MySQL篇

「老面試官」:平時用MySQL多嗎?

「小扎」:還行,基本資料儲存都用MySQL

「老面試官」:那你先說說唯一索引和普通索引區別

「小扎」:唯一索引會有唯一性校驗,而普通的索引沒有這個校驗,但是在索引的結構上,它們沒什麼區別,都是B+樹索引

「老面試官」:可以從插入和查詢方面來介紹他們的區別嗎?

「小扎」:(。。。果然不簡單)好的,從插入方面來說,由於唯一索引需要唯一性,因此要先確認資料是否已經存在,但是普通索引就不需要檢驗唯一性,找到合適位置無腦插入即可。從查詢方面來說,對唯一索引來說,因為唯一性的原因,在查到第一個滿足的資料立即返回即可,但是對於普通索引來說,因為是非唯一的,那麼就不知道後面還有多少資料,只能繼續向後檢索,直至找到第一個不滿足的資料為止,因此其實非唯一索引其實是要多檢索一條資料的。

「老面試官」:嗯,change buffer瞭解嗎?瞭解的話,說說change buffer的好處。

「小扎」:(不瞭解的話,是不是出門右拐...),瞭解一點,change buffer主要是為了減少離散的磁碟IO的。

「老面試官」:哦~,細說看看。

「小扎」:首先正常我們更新一個數據的時候,如果對應的資料不在記憶體裡的話,就要先去磁碟把對應的資料頁讀到記憶體裡,然後更新,如果每次更新都要去磁碟讀一次的話,效能會稍差,於是就出現了change buffer,change buffer的好處就是即使資料不在記憶體裡,也不去磁碟上讀,把要更新的資料先放在change buffer裡,然後後臺有個執行緒定時去把change buffer的資料同步到磁碟上,這樣的話,當同一個資料頁上發生多次變更,只需要merge一次到磁碟上。

「老面試官」:等等,你說後臺有個執行緒定時去把資料merge到磁碟上?那如果還沒來的及merge,另一個執行緒發生了讀,該怎麼辦?

「小扎」:如果執行緒還沒來得及同步,但是又發生了讀操作,那麼也會觸發把change buffer的資料merge到磁碟的事件。

「老面試官」:那是不是所有的索引資料都用change buffer就可以得到極大的收益?

「小扎」:(有坑),不是的,唯一索引就用不到,唯一索引必須要要確認資料的唯一性,因此如果資料不在記憶體裡,就必須去磁碟讀取資料確認是否已經存在。

「老面試官」:(8錯8錯,難道我的格子襯衣要退休了嗎),那你說說InnoDB儲存引擎的隔離級別吧~

「小扎」:有讀未提交、讀提交、可重複讀、序列化。

「老面試官」:讀提交會造成什麼?

「小扎」:讀提交主要會造成不可重複讀的問題。(想讓我掉坑嗎,幻讀是屬於可重複讀的,小菜一碟)。

「老面試官」:說說MySQL中DECIMAL(M,D) 中M和D分別代表什麼?

「小扎」:M是最大位數,D是小數點右邊的位數 比如decimal(5,2),最大可以表示999.99。

「老面試官」:那你再說說varchar和char的區別吧

「小扎」:varchar是變長,char是定長,在存取方面定長的char更快一點,但是當用char來儲存的資料位數不夠時,會導致用空格填充,同時char最大可儲存255個 字元 長度,而對於varchar來說,當儲存的 字元 串長度小於255位元組時,其需要1位元組的空間,當大於255位元組時,需要2位元組的空間,varchar最多能儲存65535個 位元組 的資料。

「老面試官」:varchar最多能儲存65535個位元組的資料這個不對吧?

「小扎」:這個我沒說清楚,其實是這樣的,varchar 的最大長度受限於最大行長度(max row size,65535bytes),65535個位元組包括 所有欄位 的長度,比如上面說到的如果varchar超過255個位元組的時候還需要額外的2個位元組來儲存,一個欄位如果允許為NULL,那麼還需要一個位元組來標識。因此假設表中只有一個允許為NULL的varchar型別的話,它能支援的最大長度就是65535-2-1=65532個位元組。

「老面試官」:你知道MySQL的隱式轉換嗎?

「小扎」:你說的應該就是如果索引欄位是int型,但是查詢的時候傳過來的是字元型吧?

「老面試官」:算是吧,你說說看。

「小扎」:如果是這樣情況的話,字元型會被轉換成int型,依然能夠使用到索引。

「老面試官」:那如果索引欄位是字元型,但是傳過來的是個整型數字呢?

img

「小扎」:(嘿嘿,早知道來這一套)那這時候是用不到索引的,當出現字串和數字比較的時候,MySQL會把字串轉換成數字,那麼這時候索引就失效了。

「老面試官」:你一般都是如何建立索引的,比如怎麼判斷哪些欄位需要加索引?

「小扎」:最簡單的就是經常需要出現在where條件中的欄位會加索引,同時一些重複度不高的欄位最好不要加索引,比如像性別欄位

「老面試官」:等等,這裡能說說為什麼性別這樣的欄位不適合加索引嗎?

「小扎」:這個是這樣的,假設現在有張表,表中有10w資料並且男女各5w,如果走性別索引的話,那麼就要回表5w次,關鍵這5w次的回表對於主鍵索引來說,每次都要從根節點開始往下找,這時MySQL會覺得這樣效率不高,搞不好還沒有全表掃描來的快,最起碼全表掃描只要順著葉子節點一直向後找即可,因此這時就用不到性別索引,還白白浪費了對性別索引的維護。

老面試官滿意地笑了笑,然後起身和小扎說了下,你這等一下吧。“好的”,小扎連連點頭,小扎心裡想接下來最多就是技術總監來面了,但是一般技術總監都不會問這麼細的知識的,暗暗竊喜的小扎萬萬沒想到接下來這位更剛的技術總監會問他...。

未完待續...

最後

碼字不易,各位的 點贊在看 就是作者最大的創作動力,我們下期見。

推薦閱讀:

程式設計師小扎-位元跳動公司的一面

內功大增!從機械硬碟和固態硬碟的結構來看IO

你有沒有在MySQL的order by上栽過跟頭