小林當面試官,面你 Redis !
時間:2021-08-19 16:30:42
手機看文章
掃描二維碼
隨時隨地手機看文章
[導讀]我從業(yè)多年,有參加過面試,有面試過別人,經(jīng)歷過的面試不下百場。在字節(jié)跳動的時候,作為資深面試官,深度參與校招和社招。很多人問我,面試到底考察什么?面試官究竟想聽到怎樣的回答?針對這類疑惑,我覺得最好的解答,無疑是帶著大家,以面試官視角,去進行面試,知己知彼,百戰(zhàn)不殆,這就是我寫這...
我從業(yè)多年,有參加過面試,有面試過別人,經(jīng)歷過的面試不下百場。在字節(jié)跳動的時候,作為資深面試官,深度參與校招和社招。
很多人問我,面試到底考察什么?面試官究竟想聽到怎樣的回答?針對這類疑惑,我覺得最好的解答,無疑是帶著大家,以面試官視角,去進行面試,知己知彼,百戰(zhàn)不殆,這就是我寫這個系列的初衷。
話不多說,接下來就來看看我們面試官系列的第一彈吧!
這次我們的面試主題是Redis,我一般要考察的知識點都在下圖,根據(jù)候選人的情況,會選擇不同的知識點進行提問。
通過上圖,大家對Redis面試問題也心里有數(shù)了吧?
現(xiàn)在,就讓我們來體驗一場沉浸式面試,我擔任惡魔面試官,友情請來阿柴擔任面試者。
本文分為六個部分進行:
準備好了么?Let's go!
PART1 基礎(chǔ)摸底一般情況下,面試官不會上來就問難度頗高的問題,都是隨著知識點循序漸進,校招更是遵從這樣的引導思路。所以,考察面試者都是從基礎(chǔ)知識入手,Redis當然也不例外。
你知道Redis是什么嗎?Redis是一個數(shù)據(jù)庫,不過與傳統(tǒng)RDBM不同,Redis屬于NoSQL,也就是非關(guān)系型數(shù)據(jù)庫,它的存儲結(jié)構(gòu)是Key-Value。Redis的數(shù)據(jù)直接存在內(nèi)存中,讀寫速度非??欤虼?Redis被廣泛應用于緩存方向。
那NoSQL的BASE理論是什么?可以說BASE理論是CAP中一致性的妥協(xié)。和傳統(tǒng)事務的ACID截然不同,BASE不追求強一致性,而是允許數(shù)據(jù)在一段時間內(nèi)是不一致的,但最終達到一致狀態(tài),從而獲得更高的可用性和性能。
先說說你最常用的Redis命令吧?我最常用的讀操作是get a,表示獲取a對應的數(shù)據(jù);最常用的寫操作是setex a t b,表示將a的數(shù)據(jù)設置為b,并且在t秒后過期。
你提到了過期時間,那你知道Redis的過期鍵清除策略嗎?
過期鍵清除策略有三種,分別是定時刪除、定期刪除和惰性刪除。
定時刪除,是在設置鍵的過期時間的同時,創(chuàng)建一個定時器,讓定時器在鍵的過期時間來臨時,立即執(zhí)行對鍵的刪除操作;
定期刪除,每隔一段時間,程序就對數(shù)據(jù)庫進行一次檢查,刪除里面的過期鍵;
惰性刪除,是指使用的時候,發(fā)現(xiàn)Key過期了,此時再進行刪除。
Redis過期鍵采用的是定期刪除 惰性刪除二者結(jié)合的方式進行刪除的。
如果過期鍵沒有被訪問,而周期性刪除又跟不上新鍵產(chǎn)生的速度,內(nèi)存不就慢慢耗盡了嗎?
Redis支持內(nèi)存淘汰,配置參數(shù)maxmemory_policy決定了內(nèi)存淘汰策略的策略。這個參數(shù)一共有8個枚舉值。
內(nèi)存淘汰用到的是LRU算法嗎?嗯...Redis用的是近似LRU算法,LRU算法需要一個雙向鏈表來記錄數(shù)據(jù)的最近被訪問順序,但是出于節(jié)省內(nèi)存的考慮,Redis的LRU算法并非完整的實現(xiàn)。
Redis通過對少量鍵進行取樣,然后和目前維持的淘汰池綜合比較,回收其中的最久未被訪問的鍵。通過調(diào)整每次回收時的采樣數(shù)量maxmemory-samples,可以實現(xiàn)調(diào)整算法的精度。
顯然,面試官開門見山,摸了下阿柴的基礎(chǔ),并進行了一定程度的發(fā)散。看得出來,阿柴基礎(chǔ)扎實,有備而來,是時候探探他的真本事了。
PART2 數(shù)據(jù)結(jié)構(gòu)
你知道Redis有多少數(shù)據(jù)結(jié)構(gòu)嗎??對外暴露5種Redis對象,分別是String、List、Hash、Set、Zset。底層實現(xiàn)依托于sds、ziplist、skiplist、dict等更基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)。
不錯,知道得很清楚,能區(qū)分對外結(jié)構(gòu)和底層實現(xiàn)。那么下面就針對幾個結(jié)構(gòu),來重點追問下。
那Redis字符串有什么特點?Redis的字符串如果保存的對象是整數(shù)類型,那么就用int存儲。如果不能用整數(shù)表示,就用SDS來表示,SDS通過記錄長度,和預分配空間,可以高效計算長度,進行append操作。
嗯,能說清楚SDS就達到了面試官的預期,如果能根據(jù)不同數(shù)據(jù)類型講清楚,加分!
Hash擴容過程是怎樣的?兩張Hash表,平常起作用的都是0號表,當裝載因子超過閾值時就會進行Rehash,將0號每上每一個bucket慢慢移動到1號表,所以叫漸進式Rehash,這種方式可以減少遷移系統(tǒng)的影響。
慢慢移動?能詳細說下Rehash過程嗎?
當周期函數(shù)發(fā)現(xiàn)為裝載因子超過閾值時就會進行Rehash。Rehash的流程大概分成三步。
首先,生成新Hash表ht[1],為 ht[1] 分配空間。此時字典同時持有ht[0]和ht[1]兩個哈希表。字典的偏移索引從靜默狀態(tài)-1,設置為0,表示Rehash 工作正式開始。
然后,遷移ht[0]數(shù)據(jù)到ht[1]。在 Rehash進行期間,每次對字典執(zhí)行增刪查改操作,程序會順帶遷移一個ht[0]上的數(shù)據(jù),并更新偏移索引。與此同時,周期函數(shù)也會定時遷移一批。
最后,ht[1]和ht[0]指針對象交換。隨著字典操作的不斷執(zhí)行, 最終在某個時間點上,ht[0]的所有鍵值對都會被Rehash至 ht[1],此時再將ht[1]和ht[0]指針對象互換,同時把偏移索引的值設為-1,表示Rehash操作已完成。
如果字典正在Rehash,此時有請求過來,Redis會怎么處理?
針對新增Key,是往ht[1]里面插入。針對讀請求,先從ht[0]讀,沒找到再去ht[1]找。至于刪除和更新,其實本質(zhì)是先找到位置,再進行操作,所以和讀請求一樣,先找ht[0],再找ht[1],找到之后再進行操作。
接下來講講跳表的實現(xiàn)吧。跳表本質(zhì)上是對鏈表的一種優(yōu)化,通過逐層跳步采樣的方式構(gòu)建索引,以加快查找速度。如果只用普通鏈表,只能一個一個往后找。跳表就不一樣了,可以高層索引,一次跳躍多個節(jié)點,如果找過頭了,就用更下層的索引。
圖片來自維基百科
那每個節(jié)點有多少層呢?使用概率均衡的思路,確定新插入節(jié)點的層數(shù)。Redis使用隨機函數(shù)決定層數(shù)。直觀上來說,默認1層,和丟硬幣一樣,如果是正面就繼續(xù)往上,這樣持續(xù)迭代,最大層數(shù)32層。
可以看到,50%的概率被分配到第一層,25%的概率被分配到第二層,12.5%的概率被分配到第三層。這種方式保證了越上層數(shù)量越少,自然跨越起來越方便。
跳表原理說清楚就達標,說出層次算法加分。
Redis的Zset為什么同時需要字典和跳表來實現(xiàn)?
Zset是一個有序列表,字典和跳表分別對應兩種查詢場景,字典用來支持按成員查詢數(shù)據(jù),跳表則用以實現(xiàn)高效的范圍查詢,這樣兩個場景,性能都做到了極致。
Redis的數(shù)據(jù)結(jié)構(gòu)包含了很多干貨,在細節(jié)處見功夫,阿柴對答如流。面試官不由地更加期待他接下來的表現(xiàn)了。
PART3 系統(tǒng)容災
Redis是基于內(nèi)存的存儲,如果服務重啟,數(shù)據(jù)不就丟失了嗎?
可以通過持久化機制,備份數(shù)據(jù)。有兩種方式,一種是開啟RDB,RDB是Redis的二進制快照文件,優(yōu)點是文件緊湊,占用空間小,恢復速度比較快。同時,由于是子進程Fork的模式,對Redis本身讀寫性能的影響很小。
另一種方式是AOF,AOF中記錄了Redis的操作命令,可以重放請求恢復現(xiàn)場,AOF的文件會比RDB大很多。
出于性能考慮,如果開啟了AOF,會將命令先記錄在AOF緩沖,之后再刷入磁盤。數(shù)據(jù)刷入磁盤的時機根據(jù)參數(shù)決定,有三種模式:1.關(guān)閉時刷入;2.每秒定期刷入;3.執(zhí)行命令后立刻觸發(fā)。
AOF的優(yōu)點是故障情況下,丟失的數(shù)據(jù)會比RDB更少。如果是執(zhí)行命令后立馬刷入,AOF會拖累執(zhí)行速度,所以一般都是配置為每秒定期刷入,這樣對性能影響不會很大。
AOF運行原理-創(chuàng)建
這樣看起來,AOF文件會越來越大,最后磁盤都裝不下?
不會的,Redis可以在AOF文件體積變得過大時,自動地在后臺Fork一個子進程,專門對AOF進行重寫。說白了,就是針對相同Key的操作,進行合并,比如同一個Key的set操作,那就是后面覆蓋前面。
在重寫過程中,Redis不但將新的操作記錄在原有的AOF緩沖區(qū),而且還會記錄在AOF重寫緩沖區(qū)。一旦新AOF文件創(chuàng)建完畢,Redis 就會將重寫緩沖區(qū)內(nèi)容,追加到新的AOF文件,再用新AOF文件替換原來的AOF文件。
Redis機器掛掉怎么辦?可以用主從模式部署,即有一個或多個備用機器,備用機會作為Slave同步Master的數(shù)據(jù),在Redis出現(xiàn)問題的時候,把Slave升級為Master。
主從可以自動切換嗎?本身是不能,但可以寫腳本實現(xiàn),只是需要考慮的問題比較多。Redis已經(jīng)有了現(xiàn)成的解決方案:哨兵模式。哨兵來監(jiān)測Redis服務是否正常,異常情況下,由哨兵代理切換。為避免哨兵成為單點,哨兵也需要多機部署。
如果Master掛掉,會選擇哪個Slave呢?
當哨兵集群選舉出哨兵Leader后,由哨兵Leader從Redis從節(jié)點中選擇一個Redis節(jié)點作為主節(jié)點:
1.過濾故障的節(jié)點;2.選擇優(yōu)先級slave-priority最大的從節(jié)點作為主節(jié)點,如不存在,則繼續(xù);
3.選擇復制偏移量最大的從節(jié)點作為主節(jié)點,如果都一樣,則繼續(xù)。這里解釋下,數(shù)據(jù)偏移量記錄寫了多少數(shù)據(jù),主服務器會把偏移量同步給從服務器,當主從的偏移量一致,則數(shù)據(jù)是完全同步;
4.選擇runid最小的從節(jié)點作為主節(jié)點。Redis每次啟動的時候生成隨機的runid作為Redis的標識。
前面你提到了哨兵Leader,那它是怎么來的呢?
每一個哨兵節(jié)點都可以成為Leader,當一個哨兵節(jié)點確認Redis集群的主節(jié)點主觀下線后,會請求其他哨兵節(jié)點要求將自己選舉為Leader。被請求的哨兵節(jié)點如果沒有同意過其他哨兵節(jié)點的選舉請求,則同意該請求,也就是選舉票數(shù) 1,否則不同意。
如果一個哨兵節(jié)點獲得的選舉票數(shù)超過節(jié)點數(shù)的一半,且大于quorum配置的值,則該哨兵節(jié)點選舉為Leader;否則重新進行選舉。
PART4 性能優(yōu)化
Redis性能怎么樣?只能說在十萬級。使用之前,要跑BenchMark,實際情況下會受帶寬、負載、單個數(shù)據(jù)大小、是否開啟多線程等因素影響,脫開具體場景談性能,就沒有意義。
Redis性能這么高,那它是協(xié)程模型,還是多線程模型?
Redis是單線程Reactor模型,通過高效的IO復用以及內(nèi)存處理實現(xiàn)高性能。如果是6.0之前我會毫不猶豫說是單線程,6.0之后,我還是會說單線程,但會補充一句,IO解包通過多線程進行了優(yōu)化,而處理邏輯,還是單線程。
另外,如果考慮到RDB的Fork,一些定時任務的處理,那么Redis也可以說多進程,這沒有問題。但是Redis對數(shù)據(jù)的處理,至始至終,都是單線程。
可以詳細說下6.0版本發(fā)布的多線程功能嗎?
多線程功能,主要用于提高解包的效率。和傳統(tǒng)的Multi Reactor多線程模型不同,Redis的多線程只負責處理網(wǎng)絡IO的解包和協(xié)議轉(zhuǎn)換,一方面是因為Redis的單線程處理足夠快,另一方面也是為了兼容性做考慮。
如果數(shù)據(jù)太大,Redis存不下了怎么辦?
使用集群模式。也就是將數(shù)據(jù)分片,不同的Key根據(jù)Hash路由到不同的節(jié)點。集群索引是通過一致性Hash算法來完成,這種算法可以解決服務器數(shù)量發(fā)生改變時,所有的服務器緩存在同一時間失效的問題。
同時,基于Gossip協(xié)議,集群狀態(tài)變化時,如新節(jié)點加入、節(jié)點宕機、Slave提升為新Master,這些變化都能傳播到整個集群的所有節(jié)點并達成一致。
一致性Hash能詳細講一下嗎?好的,傳統(tǒng)的Hash分片,可以將某個Key,落到某個節(jié)點。但有一個問題,當節(jié)點擴容或者縮容,路由會被完全打亂。如果是緩存場景,很容易造成緩存雪崩問題。
為了優(yōu)化這種情況,一致性Hash就應運而生了。一致性Hash是說將數(shù)據(jù)和服務器,以相同的Hash函數(shù),映射到同一個Hash環(huán)上,針對一個對象,在哈希環(huán)上順時針查找距其最近的機器,這個機器就負責處理該對象的相關(guān)請求。
這種情況下,增加節(jié)點,只會分流后面一個節(jié)點的數(shù)據(jù)。減少節(jié)點,那么請求會由后一個節(jié)點繼承。也就是說,節(jié)點變化操作,最多只會影響后面一個節(jié)點的數(shù)據(jù)。
好了,看你對Redis掌握還不錯,下面我們來聊聊場景相關(guān)的應用吧。
PART5 場景應用
Redis經(jīng)常用作緩存,那數(shù)據(jù)一致性怎么保證?
從設計思路來說,有Cache Aside和Read/Write Through兩種模式,前者是把緩存責任交給應用層,后者是將緩存的責任,放置到服務提供方。
你說到兩種模式,那么哪種模式更好呢?
兩種模式各有優(yōu)缺點,從透明性考慮,服務方比較合適;如果從性能極致來說,業(yè)務方會更有優(yōu)勢,畢竟可以減去服務RPC的損耗。
嗯,如果數(shù)據(jù)發(fā)生變化,你會怎樣去更新緩存?
更新方式的話,大概有四種:
1.數(shù)據(jù)存到數(shù)據(jù)庫中,成功后,再讓緩存失效,等到讀緩存不命中的時候,再加載進去;
2.通過消息隊列更新緩存;3.先更新緩存,再更新服務,這種情況相當于把Cache也做Buffer用;
4.起一個同步服務,作為MySQL一個從節(jié)點,通過解析binlog同步重要緩存。
那我們來談談Redis緩存雪崩。緩存雪崩表示在某一時間段,緩存集中失效,導致請求全部走數(shù)據(jù)庫,有可能搞垮數(shù)據(jù)庫,使整個服務癱瘓。雪崩原因一般是由于緩存過期時間設置得相同造成的。
針對這種情況,可以借鑒ETCD中Raft選舉的優(yōu)化,讓過期時間隨機化,避免同一批請求,在同一時間過期。另一方面,還可以業(yè)務層面容災,為熱點數(shù)據(jù)使用雙緩存。
那Redis緩存穿透又是什么?緩存穿透指請求數(shù)據(jù)庫里面根本沒有的數(shù)據(jù),高頻請求不存在的Key,有可能是正常的業(yè)務邏輯,但更可能的,是黑客的攻擊。
可以用布隆過濾器來應對,布隆過濾器是一種比較巧妙的概率型數(shù)據(jù)結(jié)構(gòu),特點是高效地插入和查詢,可以用來告訴我們?“某樣東西一定不存在或者可能存在”。
那你說一下布隆過濾器的實現(xiàn)吧。布隆過濾器底層是一個64位的整型,將字符串用多個Hash函數(shù)映射不同的二進制位置,將整型中對應位置設置為1。
在查詢的時候,如果一個字符串所有Hash函數(shù)映射的值都存在,那么數(shù)據(jù)可能存在。為什么說可能呢,就是因為其他字符可能占據(jù)該值,提前點亮。
可以看到,布隆過濾器優(yōu)缺點都很明顯,優(yōu)點是空間、時間消耗都很小,缺點是結(jié)果不是完全準確。
還有個概念叫緩存擊穿,你能講講看嗎?
嗯...緩存擊穿,是指某一熱鍵,被超高的并發(fā)訪問,在失效的一瞬間,還沒來得及重新產(chǎn)生,就有海量數(shù)據(jù),直達數(shù)據(jù)庫,可謂是兵臨城下。
這種情況和緩存雪崩的不同之處,在于雪崩是大量緩存趕巧兒一起過期,擊穿只是單個超熱鍵失效。
這種超高頻Key,我們要提高待遇,可以讓它不過期,再單獨實現(xiàn)數(shù)據(jù)同步邏輯,來維護數(shù)據(jù)的一致性。當然,無論如何,對后端肯定是需要限頻的,不然如果Redis數(shù)據(jù)丟失,數(shù)據(jù)庫還是會被打崩。限頻方式可以是分布式鎖或分布式令牌桶。
那Redis可以做消息隊列嗎?
嗯...可以是可以,但我覺得用它來做消息隊列不合適。Redis本身沒有支持AMQP規(guī)范,消息隊列該有的能力缺胳膊少腿,消息可靠性不強。
因為總有人拿Redis做消息隊列。Redis的作者都看不下去了,趕緊出了個Disque來專事專做,雖然沒大紅大紫,但至少明確告訴了我們,Redis,別拿來做消息隊列!
那你能談談Redis在秒殺場景的應用嗎?
Redis主要是起到選拔流量的作用,記錄商品總數(shù),還有就是已下單數(shù),等達到總數(shù)之后攔截所有請求。可以多放些請求進來,然后塞入消息隊列。
螞蟻金服的云Redis提到消息隊列可以用Redis來實現(xiàn),但我覺得更好的方式是用Kafka這種標準消息隊列組件。
你能繼續(xù)說說Redis在分布式鎖中的應用嗎?
鎖是計算機領(lǐng)域一個非常常見的概念,分布式鎖也依賴存儲組件,針對請求量的不同,可以選擇Etcd、MySQL、Redis等。前兩者可靠性更強,Redis性能更高。
那我們再聊聊Redis在限流場景的應用吧。
在微服務架構(gòu)下,限頻器也需要分布式化。無論是哪種算法,都可以結(jié)合Redis來實現(xiàn)。這里我比較熟悉的是基于Redis的分布式令牌桶。
很顯然,Redis負責管理令牌,微服務需要進行函數(shù)操作,就向Redis申請令牌,如果Redis當前還有令牌,就發(fā)放給它。拿到令牌,才能進行下一步操作。
另一方面,令牌不光要消耗,還需要補充,出于性能考慮,可以使用懶生成的方式:使用令牌時,順便生成令牌。這樣子還有個好處:令牌的獲取,和令牌的生成,都可以在一個Lua腳本中,保證了原子性。
可以了,你就是我們想要的仔。
PART6 面試點評
Redis是后臺開發(fā)中非常重要的領(lǐng)域,更是面試環(huán)節(jié)的高頻考點,希望通過這種形式,讓大家切身體會到面試的要點在哪里,并針對這些做專門的準備。
本次阿柴憑借著極高的修為,擊退了惡魔面試官牛牛,但這不是結(jié)束,僅僅是個開端。