本篇文章給大家整理分享一些Redis高頻面試題,帶大家過(guò)一遍Redis核心知識(shí)點(diǎn),涉及到數(shù)據(jù)結(jié)構(gòu)、內(nèi)存模型、 IO 模型、持久化 RDB等,希望對(duì)大家有所幫助!
Redis 為什么這么快?
很多人只知道是 K/V NoSQl 內(nèi)存數(shù)據(jù)庫(kù),單線程……這都是沒(méi)有全面理解 Redis 導(dǎo)致無(wú)法繼續(xù)深問(wèn)下去。
這個(gè)問(wèn)題是基礎(chǔ)摸底,我們可以從 Redis 不同數(shù)據(jù)類型底層的數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)、完全基于內(nèi)存、IO 多路復(fù)用網(wǎng)絡(luò)模型、線程模型、漸進(jìn)式 rehash……
到底有多快?
我們可以先說(shuō)到底有多快,根據(jù)官方數(shù)據(jù),Redis 的 QPS 可以達(dá)到約 100000(每秒請(qǐng)求數(shù)),有興趣的可以參考官方的基準(zhǔn)程序測(cè)試《How fast is Redis?》,地址:redis.io/topics/benc…
橫軸是連接數(shù),縱軸是 QPS。
這張圖反映了一個(gè)數(shù)量級(jí),通過(guò)量化讓面試官覺(jué)得你有看過(guò)官方文檔,很?chē)?yán)謹(jǐn)。
基于內(nèi)存實(shí)現(xiàn)
Redis 是基于內(nèi)存的數(shù)據(jù)庫(kù),跟磁盤(pán)數(shù)據(jù)庫(kù)相比,完全吊打磁盤(pán)的速度。
不論讀寫(xiě)操作都是在內(nèi)存上完成的,我們分別對(duì)比下內(nèi)存操作與磁盤(pán)操作的差異。
磁盤(pán)調(diào)用
內(nèi)存操作
內(nèi)存直接由 CPU 控制,也就是 CPU 內(nèi)部集成的內(nèi)存控制器,所以說(shuō)內(nèi)存是直接與 CPU 對(duì)接,享受與 CPU 通信的最優(yōu)帶寬。
最后以一張圖量化系統(tǒng)的各種延時(shí)時(shí)間(部分?jǐn)?shù)據(jù)引用 Brendan Gregg)
高效的數(shù)據(jù)結(jié)構(gòu)
學(xué)習(xí) MySQL 的時(shí)候我知道為了提高檢索速度使用了 B+ Tree 數(shù)據(jù)結(jié)構(gòu),所以 Redis 速度快應(yīng)該也跟數(shù)據(jù)結(jié)構(gòu)有關(guān)。
Redis 一共有 5 種數(shù)據(jù)類型,String、List、Hash、Set、SortedSet
。
不同的數(shù)據(jù)類型底層使用了一種或者多種數(shù)據(jù)結(jié)構(gòu)來(lái)支撐,目的就是為了追求更快的速度。
碼哥寄語(yǔ):我們可以分別說(shuō)明每種數(shù)據(jù)類型底層的數(shù)據(jù)結(jié)構(gòu)優(yōu)點(diǎn),很多人只知道數(shù)據(jù)類型,而說(shuō)出底層數(shù)據(jù)結(jié)構(gòu)就能讓人眼前一亮。
SDS 簡(jiǎn)單動(dòng)態(tài)字符串優(yōu)勢(shì)
-
SDS 中 len 保存這字符串的長(zhǎng)度,O(1) 時(shí)間復(fù)雜度查詢字符串長(zhǎng)度信息。
-
空間預(yù)分配:SDS 被修改后,程序不僅會(huì)為 SDS 分配所需要的必須空間,還會(huì)分配額外的未使用空間。
-
惰性空間釋放:當(dāng)對(duì) SDS 進(jìn)行縮短操作時(shí),程序并不會(huì)回收多余的內(nèi)存空間,而是使用 free 字段將這些字節(jié)數(shù)量記錄下來(lái)不釋放,后面如果需要 append 操作,則直接使用 free 中未使用的空間,減少了內(nèi)存的分配。
zipList 壓縮列表
壓縮列表是 List 、hash、 sorted Set 三種數(shù)據(jù)類型底層實(shí)現(xiàn)之一。
當(dāng)一個(gè)列表只有少量數(shù)據(jù)的時(shí)候,并且每個(gè)列表項(xiàng)要么就是小整數(shù)值,要么就是長(zhǎng)度比較短的字符串,那么 Redis 就會(huì)使用壓縮列表來(lái)做列表鍵的底層實(shí)現(xiàn)。
這樣內(nèi)存緊湊,節(jié)約內(nèi)存。
quicklist
后續(xù)版本對(duì)列表數(shù)據(jù)結(jié)構(gòu)進(jìn)行了改造,使用 quicklist 代替了 ziplist 和 linkedlist。
quicklist 是 ziplist 和 linkedlist 的混合體,它將 linkedlist 按段切分,每一段使用 ziplist 來(lái)緊湊存儲(chǔ),多個(gè) ziplist 之間使用雙向指針串接起來(lái)。
skipList 跳躍表
sorted set 類型的排序功能便是通過(guò)「跳躍列表」數(shù)據(jù)結(jié)構(gòu)來(lái)實(shí)現(xiàn)。
跳躍表(skiplist)是一種有序數(shù)據(jù)結(jié)構(gòu),它通過(guò)在每個(gè)節(jié)點(diǎn)中維持多個(gè)指向其他節(jié)點(diǎn)的指針,從而達(dá)到快速訪問(wèn)節(jié)點(diǎn)的目的。
跳表在鏈表的基礎(chǔ)上,增加了多層級(jí)索引,通過(guò)索引位置的幾個(gè)跳轉(zhuǎn),實(shí)現(xiàn)數(shù)據(jù)的快速定位,如下圖所示:
整數(shù)數(shù)組(intset)
當(dāng)一個(gè)集合只包含整數(shù)值元素,并且這個(gè)集合的元素?cái)?shù)量不多時(shí),Redis 就會(huì)使用整數(shù)集合作為集合鍵的底層實(shí)現(xiàn),節(jié)省內(nèi)存。
單線程模型
碼哥寄語(yǔ):我們需要注意的是,Redis 的單線程指的是 Redis 的網(wǎng)絡(luò) IO (6.x 版本后網(wǎng)絡(luò) IO 使用多線程)以及鍵值對(duì)指令讀寫(xiě)是由一個(gè)線程來(lái)執(zhí)行的。 對(duì)于 Redis 的持久化、集群數(shù)據(jù)同步、異步刪除等都是其他線程執(zhí)行。
千萬(wàn)別說(shuō) Redis 就只有一個(gè)線程。
單線程指的是 Redis 鍵值對(duì)讀寫(xiě)指令的執(zhí)行是單線程。
先說(shuō)官方答案,讓人覺(jué)得足夠嚴(yán)謹(jǐn),而不是人云亦云去背誦一些博客。
官方答案:因?yàn)?Redis 是基于內(nèi)存的操作,CPU 不是 Redis 的瓶頸,Redis 的瓶頸最有可能是機(jī)器內(nèi)存的大小或者網(wǎng)絡(luò)帶寬。既然單線程容易實(shí)現(xiàn),而且 CPU 不會(huì)成為瓶頸,那就順理成章地采用單線程的方案了。原文地址:redis.io/topics/faq。
為啥不用多線程執(zhí)行充分利用 CPU 呢?
在運(yùn)行每個(gè)任務(wù)之前,CPU 需要知道任務(wù)在何處加載并開(kāi)始運(yùn)行。也就是說(shuō),系統(tǒng)需要幫助它預(yù)先設(shè)置 CPU 寄存器和程序計(jì)數(shù)器,這稱為 CPU 上下文。
切換上下文時(shí),我們需要完成一系列工作,這是非常消耗資源的操作。
引入多線程開(kāi)發(fā),就需要使用同步原語(yǔ)來(lái)保護(hù)共享資源的并發(fā)讀寫(xiě),增加代碼復(fù)雜度和調(diào)試難度。
單線程又什么好處?
- 不會(huì)因?yàn)榫€程創(chuàng)建導(dǎo)致的性能消耗;
- 避免上下文切換引起的 CPU 消耗,沒(méi)有多線程切換的開(kāi)銷;
- 避免了線程之間的競(jìng)爭(zhēng)問(wèn)題,比如添加鎖、釋放鎖、死鎖等,不需要考慮各種鎖問(wèn)題。
- 代碼更清晰,處理邏輯簡(jiǎn)單。
I/O 多路復(fù)用模型
Redis 采用 I/O 多路復(fù)用技術(shù),并發(fā)處理連接。采用了 epoll + 自己實(shí)現(xiàn)的簡(jiǎn)單的事件框架。
epoll 中的讀、寫(xiě)、關(guān)閉、連接都轉(zhuǎn)化成了事件,然后利用 epoll 的多路復(fù)用特性,絕不在 IO 上浪費(fèi)一點(diǎn)時(shí)間。
Redis 線程不會(huì)阻塞在某一個(gè)特定的監(jiān)聽(tīng)或已連接套接字上,也就是說(shuō),不會(huì)阻塞在某一個(gè)特定的客戶端請(qǐng)求處理上。正因?yàn)榇?,Redis 可以同時(shí)和多個(gè)客戶端連接并處理請(qǐng)求,從而提升并發(fā)性。
Redis 全局 hash 字典
Redis 整體就是一個(gè) 哈希表來(lái)保存所有的鍵值對(duì),無(wú)論數(shù)據(jù)類型是 5 種的任意一種。哈希表,本質(zhì)就是一個(gè)數(shù)組,每個(gè)元素被叫做哈希桶,不管什么數(shù)據(jù)類型,每個(gè)桶里面的 entry 保存著實(shí)際具體值的指針。
而哈希表的時(shí)間復(fù)雜度是 O(1),只需要計(jì)算每個(gè)鍵的哈希值,便知道對(duì)應(yīng)的哈希桶位置,定位桶里面的 entry 找到對(duì)應(yīng)數(shù)據(jù),這個(gè)也是 Redis 快的原因之一。
Redis 使用對(duì)象(redisObject)來(lái)表示數(shù)據(jù)庫(kù)中的鍵值,當(dāng)我們?cè)?Redis 中創(chuàng)建一個(gè)鍵值對(duì)時(shí),至少創(chuàng)建兩個(gè)對(duì)象,一個(gè)對(duì)象是用做鍵值對(duì)的鍵對(duì)象,另一個(gè)是鍵值對(duì)的值對(duì)象。
也就是每個(gè) entry 保存著 「鍵值對(duì)」的 redisObject 對(duì)象,通過(guò) redisObject 的指針找到對(duì)應(yīng)數(shù)據(jù)。
typedef struct redisObject{ //類型 unsigned type:4; //編碼 unsigned encoding:4; //指向底層數(shù)據(jù)結(jié)構(gòu)的指針 void *ptr; //... }robj;復(fù)制代碼
Hash 沖突怎么辦?
Redis 通過(guò)鏈?zhǔn)焦?/strong>解決沖突:也就是同一個(gè) 桶里面的元素使用鏈表保存。但是當(dāng)鏈表過(guò)長(zhǎng)就會(huì)導(dǎo)致查找性能變差可能,所以 Redis 為了追求快,使用了兩個(gè)全局哈希表。用于 rehash 操作,增加現(xiàn)有的哈希桶數(shù)量,減少哈希沖突。
開(kāi)始默認(rèn)使用 「hash 表 1 」保存鍵值對(duì)數(shù)據(jù),「hash 表 2」 此刻沒(méi)有分配空間。當(dāng)數(shù)據(jù)越來(lái)多觸發(fā) rehash 操作,則執(zhí)行以下操作:
- 給 「hash 表 2 」分配更大的空間;
- 將 「hash 表 1 」的數(shù)據(jù)重新映射拷貝到 「hash 表 2」 中;
- 釋放 hash 表 1 的空間。
值得注意的是,將 hash 表 1 的數(shù)據(jù)重新映射到 hash 表 2 的過(guò)程中并不是一次性的,這樣會(huì)造成 Redis 阻塞,無(wú)法提供服務(wù)。
而是采用了漸進(jìn)式 rehash,每次處理客戶端請(qǐng)求的時(shí)候,先從「 hash 表 1」 中第一個(gè)索引開(kāi)始,將這個(gè)位置的 所有數(shù)據(jù)拷貝到 「hash 表 2」 中,就這樣將 rehash 分散到多次請(qǐng)求過(guò)程中,避免耗時(shí)阻塞。
Redis 如何實(shí)現(xiàn)持久化?宕機(jī)后如何恢復(fù)數(shù)據(jù)?
Redis 的數(shù)據(jù)持久化使用了「RDB 數(shù)據(jù)快照」的方式來(lái)實(shí)現(xiàn)宕機(jī)快速恢復(fù)。但是 過(guò)于頻繁的執(zhí)行全量數(shù)據(jù)快照,有兩個(gè)嚴(yán)重性能開(kāi)銷:
- 頻繁生成 RDB 文件寫(xiě)入磁盤(pán),磁盤(pán)壓力過(guò)大。會(huì)出現(xiàn)上一個(gè) RDB 還未執(zhí)行完,下一個(gè)又開(kāi)始生成,陷入死循環(huán)。
- fork 出 bgsave 子進(jìn)程會(huì)阻塞主線程,主線程的內(nèi)存越大,阻塞時(shí)間越長(zhǎng)。
所以 Redis 還設(shè)計(jì)了 AOF 寫(xiě)后日志記錄對(duì)內(nèi)存進(jìn)行修改的指令記錄。
面試官:什么是 RDB 內(nèi)存快照?
在 Redis 執(zhí)行「寫(xiě)」指令過(guò)程中,內(nèi)存數(shù)據(jù)會(huì)一直變化。所謂的內(nèi)存快照,指的就是 Redis 內(nèi)存中的數(shù)據(jù)在某一刻的狀態(tài)數(shù)據(jù)。
好比時(shí)間定格在某一刻,當(dāng)我們拍照的,通過(guò)照片就能把某一刻的瞬間畫(huà)面完全記錄下來(lái)。
Redis 跟這個(gè)類似,就是把某一刻的數(shù)據(jù)以文件的形式拍下來(lái),寫(xiě)到磁盤(pán)上。這個(gè)快照文件叫做 RDB 文件,RDB 就是 Redis DataBase 的縮寫(xiě)。
在做數(shù)據(jù)恢復(fù)時(shí),直接將 RDB 文件讀入內(nèi)存完成恢復(fù)。
面試官:在生成 RDB 期間,Redis 可以同時(shí)處理寫(xiě)請(qǐng)求么?
可以的,Redis 使用操作系統(tǒng)的多進(jìn)程寫(xiě)時(shí)復(fù)制技術(shù) COW(Copy On Write) 來(lái)實(shí)現(xiàn)快照持久化,保證數(shù)據(jù)一致性。
Redis 在持久化時(shí)會(huì)調(diào)用 glibc 的函數(shù)fork
產(chǎn)生一個(gè)子進(jìn)程,快照持久化完全交給子進(jìn)程來(lái)處理,父進(jìn)程繼續(xù)處理客戶端請(qǐng)求。
當(dāng)主線程執(zhí)行寫(xiě)指令修改數(shù)據(jù)的時(shí)候,這個(gè)數(shù)據(jù)就會(huì)復(fù)制一份副本, bgsave
子進(jìn)程讀取這個(gè)副本數(shù)據(jù)寫(xiě)到 RDB 文件。
這既保證了快照的完整性,也允許主線程同時(shí)對(duì)數(shù)據(jù)進(jìn)行修改,避免了對(duì)正常業(yè)務(wù)的影響。
面試官:那 AOF 又是什么?
AOF 日志記錄了自 Redis 實(shí)例創(chuàng)建以來(lái)所有的修改性指令序列,那么就可以通過(guò)對(duì)一個(gè)空的 Redis 實(shí)例順序執(zhí)行所有的指令,也就是「重放」,來(lái)恢復(fù) Redis 當(dāng)前實(shí)例的內(nèi)存數(shù)據(jù)結(jié)構(gòu)的狀態(tài)。
Redis 提供的 AOF 配置項(xiàng)appendfsync
寫(xiě)回策略直接決定 AOF 持久化功能的效率和安全性。
- always:同步寫(xiě)回,寫(xiě)指令執(zhí)行完畢立馬將
aof_buf
緩沖區(qū)中的內(nèi)容刷寫(xiě)到 AOF 文件。 - everysec:每秒寫(xiě)回,寫(xiě)指令執(zhí)行完,日志只會(huì)寫(xiě)到 AOF 文件緩沖區(qū),每隔一秒就把緩沖區(qū)內(nèi)容同步到磁盤(pán)。
- no: 操作系統(tǒng)控制,寫(xiě)執(zhí)行執(zhí)行完畢,把日志寫(xiě)到 AOF 文件內(nèi)存緩沖區(qū),由操作系統(tǒng)決定何時(shí)刷寫(xiě)到磁盤(pán)。
沒(méi)有兩全其美的策略,我們需要在性能和可靠性上做一個(gè)取舍。
面試官:既然 RDB 有兩個(gè)性能問(wèn)題,那為何不用 AOF 即可。
AOF 寫(xiě)前日志,記錄的是每個(gè)「寫(xiě)」指令操作。不會(huì)像 RDB 全量快照導(dǎo)致性能損耗,但是執(zhí)行速度沒(méi)有 RDB 快,同時(shí)日志文件過(guò)大也會(huì)造成性能問(wèn)題。
所以,Redis 設(shè)計(jì)了一個(gè)殺手锏「AOF 重寫(xiě)機(jī)制」,Redis 提供了 bgrewriteaof
指令用于對(duì) AOF 日志進(jìn)行瘦身。
其原理就是開(kāi)辟一個(gè)子進(jìn)程對(duì)內(nèi)存進(jìn)行遍歷轉(zhuǎn)換成一系列 Redis 的操作指令,序列化到一個(gè)新的 AOF 日志文件中。序列化完畢后再將操作期間發(fā)生的增量 AOF 日志追加到這個(gè)新的 AOF 日志文件中,追加完畢后就立即替代舊的 AOF 日志文件了,瘦身工作就完成了。
面試官:如何實(shí)現(xiàn) 數(shù)據(jù)盡可能少丟失又能兼顧性能呢?
重啟 Redis 時(shí),我們很少使用 rdb 來(lái)恢復(fù)內(nèi)存狀態(tài),因?yàn)闀?huì)丟失大量數(shù)據(jù)。我們通常使用 AOF 日志重放,但是重放 AOF 日志性能相對(duì) rdb 來(lái)說(shuō)要慢很多,這樣在 Redis 實(shí)例很大的情況下,啟動(dòng)需要花費(fèi)很長(zhǎng)的時(shí)間。
Redis 4.0 為了解決這個(gè)問(wèn)題,帶來(lái)了一個(gè)新的持久化選項(xiàng)——混合持久化。將 rdb 文件的內(nèi)容和增量的 AOF 日志文件存在一起。這里的 AOF 日志不再是全量的日志,而是自持久化開(kāi)始到持久化結(jié)束的這段時(shí)間發(fā)生的增量 AOF 日志,通常這部分 AOF 日志很小。
于是在 Redis 重啟的時(shí)候,可以先加載 rdb 的內(nèi)容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重啟效率因此大幅得到提升。
Redis 主從架構(gòu)數(shù)據(jù)同步
Redis 提供了主從模式,通過(guò)主從復(fù)制,將數(shù)據(jù)冗余一份復(fù)制到其他 Redis 服務(wù)器。
面試官:主從之間數(shù)據(jù)如何保證一致性?
為了保證副本數(shù)據(jù)的一致性,主從架構(gòu)采用了讀寫(xiě)分離的方式。
- 讀操作:主、從庫(kù)都可以執(zhí)行;
- 寫(xiě)操作:主庫(kù)先執(zhí)行,之后將寫(xiě)操作同步到從庫(kù);
面試官:主從復(fù)制還有其他作用么?
- 故障恢復(fù):當(dāng)主節(jié)點(diǎn)宕機(jī),其他節(jié)點(diǎn)依然可以提供服務(wù);
- 負(fù)載均衡:Master 節(jié)點(diǎn)提供寫(xiě)服務(wù),Slave 節(jié)點(diǎn)提供讀服務(wù),分擔(dān)壓力;
- 高可用基石:是哨兵和 cluster 實(shí)施的基礎(chǔ),是高可用的基石。
面試官:主從復(fù)制如何實(shí)現(xiàn)的?
同步分為三種情況:
- 第一次主從庫(kù)全量復(fù)制;
- 主從正常運(yùn)行期間的同步;
- 主從庫(kù)間網(wǎng)絡(luò)斷開(kāi)重連同步。
面試官:第一次同步怎么實(shí)現(xiàn)?
主從庫(kù)第一次復(fù)制過(guò)程大體可以分為 3 個(gè)階段:連接建立階段(即準(zhǔn)備階段)、主庫(kù)同步數(shù)據(jù)到從庫(kù)階段、發(fā)送同步期間新寫(xiě)命令到從庫(kù)階段;
- 建立連接:從庫(kù)會(huì)和主庫(kù)建立連接,從庫(kù)執(zhí)行 replicaof 并發(fā)送 psync 命令并告訴主庫(kù)即將進(jìn)行同步,主庫(kù)確認(rèn)回復(fù)后,主從庫(kù)間就開(kāi)始同步了。
- 主庫(kù)同步數(shù)據(jù)給從庫(kù):master 執(zhí)行
bgsave
命令生成 RDB 文件,并將文件發(fā)送給從庫(kù),同時(shí)主庫(kù)為每一個(gè) slave 開(kāi)辟一塊 replication buffer 緩沖區(qū)記錄從生成 RDB 文件開(kāi)始收到的所有寫(xiě)命令。從庫(kù)保存 RDB 并清空數(shù)據(jù)庫(kù)再加載 RDB 數(shù)據(jù)到內(nèi)存中。 - 發(fā)送 RDB 之后接收到的新寫(xiě)命令到從庫(kù):在生成 RDB 文件之后的寫(xiě)操作并沒(méi)有記錄到剛剛的 RDB 文件中,為了保證主從庫(kù)數(shù)據(jù)的一致性,所以主庫(kù)會(huì)在內(nèi)存中使用一個(gè)叫 replication buffer 記錄 RDB 文件生成后的所有寫(xiě)操作。并將里面的數(shù)據(jù)發(fā)送到 slave。
面試官:主從庫(kù)間的網(wǎng)絡(luò)斷了咋辦?斷開(kāi)后要重新全量復(fù)制么?
在 Redis 2.8 之前,如果主從庫(kù)在命令傳播時(shí)出現(xiàn)了網(wǎng)絡(luò)閃斷,那么,從庫(kù)就會(huì)和主庫(kù)重新進(jìn)行一次全量復(fù)制,開(kāi)銷非常大。
從 Redis 2.8 開(kāi)始,網(wǎng)絡(luò)斷了之后,主從庫(kù)會(huì)采用增量復(fù)制的方式繼續(xù)同步。
增量復(fù)制:用于網(wǎng)絡(luò)中斷等情況后的復(fù)制,只將中斷期間主節(jié)點(diǎn)執(zhí)行的寫(xiě)命令發(fā)送給從節(jié)點(diǎn),與全量復(fù)制相比更加高效。
斷開(kāi)重連增量復(fù)制的實(shí)現(xiàn)奧秘就是 repl_backlog_buffer
緩沖區(qū),不管在什么時(shí)候 master 都會(huì)將寫(xiě)指令操作記錄在 repl_backlog_buffer
中,因?yàn)閮?nèi)存有限, repl_backlog_buffer
是一個(gè)定長(zhǎng)的環(huán)形數(shù)組,如果數(shù)組內(nèi)容滿了,就會(huì)從頭開(kāi)始覆蓋前面的內(nèi)容。
master 使用 master_repl_offset
記錄自己寫(xiě)到的位置偏移量,slave 則使用 slave_repl_offset
記錄已經(jīng)讀取到的偏移量。
當(dāng)主從斷開(kāi)重連后,slave 會(huì)先發(fā)送 psync 命令給 master,同時(shí)將自己的 runID
,slave_repl_offset
發(fā)送給 master。
master 只需要把 master_repl_offset
與 slave_repl_offset
之間的命令同步給從庫(kù)即可。
增量復(fù)制執(zhí)行流程如下圖:
面試官:那完成全量同步后,正常運(yùn)行過(guò)程中如何同步數(shù)據(jù)呢?
當(dāng)主從庫(kù)完成了全量復(fù)制,它們之間就會(huì)一直維護(hù)一個(gè)網(wǎng)絡(luò)連接,主庫(kù)會(huì)通過(guò)這個(gè)連接將后續(xù)陸續(xù)收到的命令操作再同步給從庫(kù),這個(gè)過(guò)程也稱為基于長(zhǎng)連接的命令傳播,使用長(zhǎng)連接的目的就是避免頻繁建立連接導(dǎo)致的開(kāi)銷。
哨兵原理連環(huán)問(wèn)
面試官:可以呀,知道這么多,你知道 哨兵集群原理么?
哨兵是 Redis 的一種運(yùn)行模式,它專注于對(duì) Redis 實(shí)例(主節(jié)點(diǎn)、從節(jié)點(diǎn))運(yùn)行狀態(tài)的監(jiān)控,并能夠在主節(jié)點(diǎn)發(fā)生故障時(shí)通過(guò)一系列的機(jī)制實(shí)現(xiàn)選主及主從切換,實(shí)現(xiàn)故障轉(zhuǎn)移,確保整個(gè) Redis 系統(tǒng)的可用性。
他的架構(gòu)圖如下:
Redis 哨兵具備的能力有如下幾個(gè):
- 監(jiān)控:持續(xù)監(jiān)控 master 、slave 是否處于預(yù)期工作狀態(tài)。
- 自動(dòng)切換主庫(kù):當(dāng) Master 運(yùn)行故障,哨兵啟動(dòng)自動(dòng)故障恢復(fù)流程:從 slave 中選擇一臺(tái)作為新 master。
- 通知:讓 slave 執(zhí)行 replicaof ,與新的 master 同步;并且通知客戶端與新 master 建立連接。
面試官:哨兵之間是如何知道彼此的?
哨兵與 master 建立通信,利用 master 提供發(fā)布/訂閱機(jī)制發(fā)布自己的信息,比如身高體重、是否單身、IP、端口……
master 有一個(gè) __sentinel__:hello
的專用通道,用于哨兵之間發(fā)布和訂閱消息。這就好比是 __sentinel__:hello
微信群,哨兵利用 master 建立的微信群發(fā)布自己的消息,同時(shí)關(guān)注其他哨兵發(fā)布的消息。
面試官:哨兵之間雖然建立連接了,但是還需要和 slave 建立連接,不然沒(méi)法監(jiān)控他們呀,如何知道 slave 并監(jiān)控他們的?
關(guān)鍵還是利用 master 來(lái)實(shí)現(xiàn),哨兵向 master 發(fā)送 INFO
命令, master 掌門(mén)自然是知道自己門(mén)下所有的 salve 小弟的。所以 master 接收到命令后,便將 slave 列表告訴哨兵。
哨兵根據(jù) master 響應(yīng)的 slave 名單信息與每一個(gè) salve 建立連接,并且根據(jù)這個(gè)連接持續(xù)監(jiān)控哨兵。
Cluster 集群連環(huán)炮
面試官:除了哨兵以外,還有其他的高可用手段么?
有 Cluster 集群實(shí)現(xiàn)高可用,哨兵集群監(jiān)控的 Redis 集群是主從架構(gòu),無(wú)法很想拓展。使用 Redis Cluster 集群,主要解決了大數(shù)據(jù)量存儲(chǔ)導(dǎo)致的各種慢問(wèn)題,同時(shí)也便于橫向拓展。
在面向百萬(wàn)、千萬(wàn)級(jí)別的用戶規(guī)模時(shí),橫向擴(kuò)展的 Redis 切片集群會(huì)是一個(gè)非常好的選擇。
面試官:什么是 Cluster 集群?
Redis 集群是一種分布式數(shù)據(jù)庫(kù)方案,集群通過(guò)分片(sharding)來(lái)進(jìn)行數(shù)據(jù)管理(「分治思想」的一種實(shí)踐),并提供復(fù)制和故障轉(zhuǎn)移功能。
將數(shù)據(jù)劃分為 16384 的 slots,每個(gè)節(jié)點(diǎn)負(fù)責(zé)一部分槽位。槽位的信息存儲(chǔ)于每個(gè)節(jié)點(diǎn)中。
它是去中心化的,如圖所示,該集群有三個(gè) Redis 節(jié)點(diǎn)組成,每個(gè)節(jié)點(diǎn)負(fù)責(zé)整個(gè)集群的一部分?jǐn)?shù)據(jù),每個(gè)節(jié)點(diǎn)負(fù)責(zé)的數(shù)據(jù)多少可能不一樣。
三個(gè)節(jié)點(diǎn)相互連接組成一個(gè)對(duì)等的集群,它們之間通過(guò) Gossip
協(xié)議相互交互集群信息,最后每個(gè)節(jié)點(diǎn)都保存著其他節(jié)點(diǎn)的 slots 分配情況。
面試官:哈希槽又是如何映射到 Redis 實(shí)例上呢?
- 根據(jù)鍵值對(duì)的 key,使用 CRC16 算法,計(jì)算出一個(gè) 16 bit 的值;
- 將 16 bit 的值對(duì) 16384 執(zhí)行取模,得到 0 ~ 16383 的數(shù)表示 key 對(duì)應(yīng)的哈希槽。
- 根據(jù)該槽信息定位到對(duì)應(yīng)的實(shí)例。
鍵值對(duì)數(shù)據(jù)、哈希槽、Redis 實(shí)例之間的映射關(guān)系如下:
面試官:Cluster 如何實(shí)現(xiàn)故障轉(zhuǎn)移?
Redis 集群節(jié)點(diǎn)采用 Gossip
協(xié)議來(lái)廣播自己的狀態(tài)以及自己對(duì)整個(gè)集群認(rèn)知的改變。比如一個(gè)節(jié)點(diǎn)發(fā)現(xiàn)某個(gè)節(jié)點(diǎn)失聯(lián)了 (PFail),它會(huì)將這條信息向整個(gè)集群廣播,其它節(jié)點(diǎn)也就可以收到這點(diǎn)失聯(lián)信息。
如果一個(gè)節(jié)點(diǎn)收到了某個(gè)節(jié)點(diǎn)失聯(lián)的數(shù)量 (PFail Count) 已經(jīng)達(dá)到了集群的大多數(shù),就可以標(biāo)記該節(jié)點(diǎn)為確定下線狀態(tài) (Fail),然后向整個(gè)集群廣播,強(qiáng)迫其它節(jié)點(diǎn)也接收該節(jié)點(diǎn)已經(jīng)下線的事實(shí),并立即對(duì)該失聯(lián)節(jié)點(diǎn)進(jìn)行主從切換。
面試官:客戶端又怎么確定訪問(wèn)的數(shù)據(jù)到底分布在哪個(gè)實(shí)例上呢?
Redis 實(shí)例會(huì)將自己的哈希槽信息通過(guò) Gossip 協(xié)議發(fā)送給集群中其他的實(shí)例,實(shí)現(xiàn)了哈希槽分配信息的擴(kuò)散。
這樣,集群中的每個(gè)實(shí)例都有所有哈希槽與實(shí)例之間的映射關(guān)系信息。
當(dāng)客戶端連接任何一個(gè)實(shí)例,實(shí)例就將哈希槽與實(shí)例的映射關(guān)系響應(yīng)給客戶端,客戶端就會(huì)將哈希槽與實(shí)例映射信息緩存在本地。
當(dāng)客戶端請(qǐng)求時(shí),會(huì)計(jì)算出鍵所對(duì)應(yīng)的哈希槽,再通過(guò)本地緩存的哈希槽實(shí)例映射信息定位到數(shù)據(jù)所在實(shí)例上,再將請(qǐng)求發(fā)送給對(duì)應(yīng)的實(shí)例。
面試官:什么是 Redis 重定向機(jī)制?
哈希槽與實(shí)例之間的映射關(guān)系由于新增實(shí)例或者負(fù)載均衡重新分配導(dǎo)致改變了,客戶端將請(qǐng)求發(fā)送到實(shí)例上,這個(gè)實(shí)例沒(méi)有相應(yīng)的數(shù)據(jù),該 Redis 實(shí)例會(huì)告訴客戶端將請(qǐng)求發(fā)送到其他的實(shí)例上。
Redis 通過(guò) MOVED 錯(cuò)誤和 ASK 錯(cuò)誤告訴客戶端。
MOVED
MOVED 錯(cuò)誤(負(fù)載均衡,數(shù)據(jù)已經(jīng)遷移到其他實(shí)例上):當(dāng)客戶端將一個(gè)鍵值對(duì)操作請(qǐng)求發(fā)送給某個(gè)實(shí)例,而這個(gè)鍵所在的槽并非由自己負(fù)責(zé)的時(shí)候,該實(shí)例會(huì)返回一個(gè) MOVED 錯(cuò)誤指引轉(zhuǎn)向正在負(fù)責(zé)該槽的節(jié)點(diǎn)。
同時(shí),客戶端還會(huì)更新本地緩存,將該 slot 與 Redis 實(shí)例對(duì)應(yīng)關(guān)系更新正確。
ASK
如果某個(gè) slot 的數(shù)據(jù)比較多,部分遷移到新實(shí)例,還有一部分沒(méi)有遷移。
如果請(qǐng)求的 key 在當(dāng)前節(jié)點(diǎn)找到就直接執(zhí)行命令,否則時(shí)候就需要 ASK 錯(cuò)誤響應(yīng)了。
槽部分遷移未完成的情況下,如果需要訪問(wèn)的 key 所在 Slot 正在從 實(shí)例 1 遷移到 實(shí)例 2(如果 key 已經(jīng)不在實(shí)例 1),實(shí)例 1 會(huì)返回客戶端一條 ASK 報(bào)錯(cuò)信息:客戶端請(qǐng)求的 key 所在的哈希槽正在遷移到實(shí)例 2 上,你先給實(shí)例 2 發(fā)送一個(gè) ASKING 命令,接著發(fā)發(fā)送操作命令。
比如客戶端請(qǐng)求定位到 key = 「公眾號(hào):碼哥字節(jié)」的槽 16330 在實(shí)例 172.17.18.1 上,節(jié)點(diǎn) 1 如果找得到就直接執(zhí)行命令,否則響應(yīng) ASK 錯(cuò)誤信息,并指引客戶端轉(zhuǎn)向正在遷移的目標(biāo)節(jié)點(diǎn) 172.17.18.2。
注意:ASK 錯(cuò)誤指令并不會(huì)更新客戶端緩存的哈希槽分配信息。
總結(jié)
本篇主要將 Redis 核心內(nèi)容過(guò)了一遍,涉及到數(shù)據(jù)結(jié)構(gòu)、內(nèi)存模型、 IO 模型、持久化 RDB 和AOF 、主從復(fù)制原理、哨兵原理、cluster 原理。
原文地址:https://juejin.cn/post/6976257378094481444
作者:碼哥字節(jié)