在上周通過著名公司【比特跳動】的一面之后,程序員小扎迎來了至關重要的二面,很往常一樣,今天小扎的身體又不舒服了,按照慣例和領導請了半天假。由于早上要去現場面試了,早早起床的小扎快速的完成了洗漱,穿上格子襯衣,背個電腦包,騎上共享單車,提前半小時來到了比特跳動公司了?!澳愫?#xff0c;我是小扎,我是來面試的”,程序員小扎和美麗的前臺小姐姐說道,“好的,稍等,面試官還沒到,我先領你到這邊會議室,你先等會”。
在等了大概半小時后,一位大腹便便的老面試官走了進來,這時他們面面相覷,雙方都笑了,因為他們撞衫了,在尷尬之后,雙方點頭示意,老面試官一本正經的端坐在椅子上,看著小扎的簡歷,心想:至今10個人有9個跪在我這里,況且如果讓你過了,以后我這個典藏的格子襯衣還能穿嗎,今天得放大招了。小扎萬萬沒想到今天因為一件格子襯衣,讓他陷入一場苦戰。
「老面試官」:如果要實現一個排行的功能,你有什么想法?
「小扎」:可以用Redis的zset,把要參與排序的權重作為分值,設置到zset中。
「老面試官」:那如果我要知道小明的排名,該用什么命令呢?
「小扎」:(這老東西,竟然問這么細),用zrank key 小明即可獲得小明的排名。
「老面試官」:那zset的底層是什么數據結構?
「小扎」:用的跳躍表。
「老面試官」:那你給我介紹下跳躍表的好處是什么?
「小扎」:(。。。,看來這是個面霸),好的,我們先來看看沒有跳躍表的時候會怎么樣:
比如現在如果我要找7這個數字,那么由于zset是順序的,只能重頭開始一個一個找,要找到最后(1->2->3->4->5->6->7),很顯然,速度是挺慢的,但是如果現在我給1、4、7分別再加一層
那么此時找7,只需要1->4->7三步即可,大大減少了時長,這就是跳躍表的好處。
「老面試官」:那Redis的zet除了使用跳躍表還有使用其他的數據結構嗎?
「小扎」:(這是要打破砂鍋問到底呀~,今天我是和你剛上了),還有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可能阻塞的一個原因。(尷尬,竟然沒想到這點,這下他耀武揚威了)。
「老面試官」:(還不錯,能想到這個點,不過我背的面試題也就這么多,換個話題吧)
老面試官再次看了看簡歷,說到:原來你是面試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
「老面試官」:那你先說說唯一索引和普通索引區別
「小扎」:唯一索引會有唯一性校驗,而普通的索引沒有這個校驗,但是在索引的結構上,它們沒什么區別,都是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型,依然能夠使用到索引。
「老面試官」:那如果索引字段是字符型,但是傳過來的是個整型數字呢?
「小扎」:(嘿嘿,早知道來這一套)那這時候是用不到索引的,當出現字符串和數字比較的時候,MySQL會把字符串轉換成數字,那么這時候索引就失效了。
「老面試官」:你一般都是如何建立索引的,比如怎么判斷哪些字段需要加索引?
「小扎」:最簡單的就是經常需要出現在where條件中的字段會加索引,同時一些重復度高的字段最好不要加索引,比如像性別字段
「老面試官」:等等,這里能說說為什么性別這樣的字段不適合加索引嗎?
「小扎」:這個是這樣的,假設現在有張表,表中有10w數據并且男女各5w,如果走性別索引的話,那么就要回表5w次,關鍵這5w次的回表對于主鍵索引來說,每次都要從根節點開始往下找,這時MySQL會覺得這樣效率不高,搞不好還沒有全表掃描來的快,最起碼全表掃描只要順著葉子節點一直向后找即可,因此這時就用不到性別索引,還白白浪費了對性別索引的維護。
老面試官滿意地笑了笑,然后起身和小扎說了下,你這等一下吧。 “好的”,小扎連連點頭,小扎心里想接下來最多就是技術總監來面了,但是一般技術總監都不會問這么細的知識的,暗暗竊喜的小扎萬萬沒想到接下來這位更剛的技術總監會問他...。
未完待續...
碼字不易,各位的點贊就是作者最大的創作動力,敬請關注公眾號【假裝懂編程】,程序員小扎的三面會在這上面首發哦。
推薦閱讀:
|