Redis 面霸篇:從高頻問(wèn)題透視核心原理
Redis 為什么這么快?
很多人只知道是 K/V NoSQl 內(nèi)存數(shù)據(jù)庫(kù),單線(xiàn)程……這都是沒(méi)有全面理解 Redis 導(dǎo)致無(wú)法繼續(xù)深問(wèn)下去。
這個(gè)問(wèn)題是基礎(chǔ)摸底,我們可以從 Redis 不同數(shù)據(jù)類(lèi)型底層的數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)、完全基于內(nèi)存、IO 多路復(fù)用網(wǎng)絡(luò)模型、線(xiàn)程模型、漸進(jìn)式 rehash…...
到底有多快?
我們可以先說(shuō)到底有多快,根據(jù)官方數(shù)據(jù),Redis 的 QPS 可以達(dá)到約 100000(每秒請(qǐng)求數(shù)),有興趣的可以參考官方的基準(zhǔn)程序測(cè)試《How fast is Redis?》,地址:https://redis.io/topics/benchmarks
基準(zhǔn)測(cè)試
橫軸是連接數(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ù)類(lèi)型,String、List、Hash、Set、SortedSet。
不同的數(shù)據(jù)類(lèi)型底層使用了一種或者多種數(shù)據(jù)結(jié)構(gòu)來(lái)支撐,目的就是為了追求更快的速度。
碼哥寄語(yǔ):我們可以分別說(shuō)明每種數(shù)據(jù)類(lèi)型底層的數(shù)據(jù)結(jié)構(gòu)優(yōu)點(diǎn),很多人只知道數(shù)據(jù)類(lèi)型,而說(shuō)出底層數(shù)據(jù)結(jié)構(gòu)就能讓人眼前一亮。
SDS 簡(jiǎn)單動(dòng)態(tài)字符串優(yōu)勢(shì)
C 語(yǔ)言字符串與 SDS
- SDS 中 len 保存這字符串的長(zhǎng)度,O(1) 時(shí)間復(fù)雜度查詢(xún)字符串長(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ù)類(lèi)型底層實(shí)現(xiàn)之一。
當(dāng)一個(gè)列表只有少量數(shù)據(jù)的時(shí)候,并且每個(gè)列表項(xiàng)要么就是小整數(shù)值,要么就是長(zhǎng)度比較短的字符串,那么 Redis 就會(huì)使用壓縮列表來(lái)做列表鍵的底層實(shí)現(xiàn)。
ziplist
這樣內(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 類(lèi)型的排序功能便是通過(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á)到快速訪(fǎng)問(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)存。
單線(xiàn)程模型
碼哥寄語(yǔ):我們需要注意的是,Redis 的單線(xiàn)程指的是 Redis 的網(wǎng)絡(luò) IO (6.x 版本后網(wǎng)絡(luò) IO 使用多線(xiàn)程)以及鍵值對(duì)指令讀寫(xiě)是由一個(gè)線(xiàn)程來(lái)執(zhí)行的。 對(duì)于 Redis 的持久化、集群數(shù)據(jù)同步、異步刪除等都是其他線(xiàn)程執(zhí)行。
千萬(wàn)別說(shuō) Redis 就只有一個(gè)線(xiàn)程。
單線(xiàn)程指的是 Redis 鍵值對(duì)讀寫(xiě)指令的執(zhí)行是單線(xiàn)程。
先說(shuō)官方答案,讓人覺(jué)得足夠嚴(yán)謹(jǐn),而不是人云亦云去背誦一些博客。
官方答案:因?yàn)?Redis 是基于內(nèi)存的操作,CPU 不是 Redis 的瓶頸,Redis 的瓶頸最有可能是機(jī)器內(nèi)存的大小或者網(wǎng)絡(luò)帶寬。既然單線(xiàn)程容易實(shí)現(xiàn),而且 CPU 不會(huì)成為瓶頸,那就順理成章地采用單線(xiàn)程的方案了。原文地址:https://redis.io/topics/faq。
為啥不用多線(xiàn)程執(zhí)行充分利用 CPU 呢?
在運(yùn)行每個(gè)任務(wù)之前,CPU 需要知道任務(wù)在何處加載并開(kāi)始運(yùn)行。也就是說(shuō),系統(tǒng)需要幫助它預(yù)先設(shè)置 CPU 寄存器和程序計(jì)數(shù)器,這稱(chēng)為 CPU 上下文。
切換上下文時(shí),我們需要完成一系列工作,這是非常消耗資源的操作。
引入多線(xiàn)程開(kāi)發(fā),就需要使用同步原語(yǔ)來(lái)保護(hù)共享資源的并發(fā)讀寫(xiě),增加代碼復(fù)雜度和調(diào)試難度。
單線(xiàn)程有什么好處?
不會(huì)因?yàn)榫€(xiàn)程創(chuàng)建導(dǎo)致的性能消耗;
避免上下文切換引起的 CPU 消耗,沒(méi)有多線(xiàn)程切換的開(kāi)銷(xiāo);
避免了線(xiàn)程之間的競(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í)間。
高性能 IO 多路復(fù)用
Redis 線(xiàn)程不會(huì)阻塞在某一個(gè)特定的監(jiān)聽(tīng)或已連接套接字上,也就是說(shuō),不會(huì)阻塞在某一個(gè)特定的客戶(hù)端請(qǐng)求處理上。正因?yàn)榇?,Redis 可以同時(shí)和多個(gè)客戶(hù)端連接并處理請(qǐng)求,從而提升并發(fā)性。
Redis 全局 hash 字典
Redis 整體就是一個(gè) 哈希表來(lái)保存所有的鍵值對(duì),無(wú)論數(shù)據(jù)類(lèi)型是 5 種的任意一種。哈希表,本質(zhì)就是一個(gè)數(shù)組,每個(gè)元素被叫做哈希桶,不管什么數(shù)據(jù)類(lèi)型,每個(gè)桶里面的 entry 保存著實(shí)際具體值的指針。
Redis 全局哈希表
而哈希表的時(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{
- //類(lèi)型
- unsigned type:4;
- //編碼
- unsigned encoding:4;
- //指向底層數(shù)據(jù)結(jié)構(gòu)的指針
- void *ptr;
- //...
- }robj;
Hash 沖突怎么辦?
Redis 通過(guò)鏈?zhǔn)焦=鉀Q沖突:也就是同一個(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,每次處理客戶(hù)端請(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)銷(xiāo):
- 頻繁生成 RDB 文件寫(xiě)入磁盤(pán),磁盤(pán)壓力過(guò)大。會(huì)出現(xiàn)上一個(gè) RDB 還未執(zhí)行完,下一個(gè)又開(kāi)始生成,陷入死循環(huán)。
- fork 出 bgsave 子進(jìn)程會(huì)阻塞主線(xiàn)程,主線(xiàn)程的內(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è)類(lèi)似,就是把某一刻的數(shù)據(jù)以文件的形式拍下來(lái),寫(xiě)到磁盤(pán)上。這個(gè)快照文件叫做 RDB 文件,RDB 就是 Redis DataBase 的縮寫(xiě)。
RDB內(nèi)存快照
在做數(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ù)處理客戶(hù)端請(qǐng)求。
當(dāng)主線(xiàn)程執(zhí)行寫(xiě)指令修改數(shù)據(jù)的時(shí)候,這個(gè)數(shù)據(jù)就會(huì)復(fù)制一份副本, bgsave 子進(jìn)程讀取這個(gè)副本數(shù)據(jù)寫(xiě)到 RDB 文件。
這既保證了快照的完整性,也允許主線(xiàn)程同時(shí)對(duì)數(shù)據(jù)進(jìn)行修改,避免了對(duì)正常業(yè)務(wù)的影響。
寫(xiě)時(shí)復(fù)制技術(shù)保證快照期間數(shù)據(jù)可修改
面試官:那 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 日志文件了,瘦身工作就完成了。
AOF重寫(xiě)機(jī)制(3條變一條)
面試官:如何實(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ù);
Redis 讀寫(xiě)分離
面試官:主從復(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ù)階段;
Redis全量同步
- 建立連接:從庫(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)銷(xiāo)非常大。
從 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)容滿(mǎn)了,就會(huì)從頭開(kāi)始覆蓋前面的內(nèi)容。
master 使用 master_repl_offset記錄自己寫(xiě)到的位置偏移量,slave 則使用 slave_repl_offset記錄已經(jīng)讀取到的偏移量。
repl_backlog_buffer
當(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í)行流程如下圖:
Redis增量復(fù)制
面試官:那完成全量同步后,正常運(yùn)行過(guò)程中如何同步數(shù)據(jù)呢?
當(dāng)主從庫(kù)完成了全量復(fù)制,它們之間就會(huì)一直維護(hù)一個(gè)網(wǎng)絡(luò)連接,主庫(kù)會(huì)通過(guò)這個(gè)連接將后續(xù)陸續(xù)收到的命令操作再同步給從庫(kù),這個(gè)過(guò)程也稱(chēng)為基于長(zhǎng)連接的命令傳播,使用長(zhǎng)連接的目的就是避免頻繁建立連接導(dǎo)致的開(kāi)銷(xiāo)。
哨兵原理連環(huán)問(wèn)
面試官:可以呀,知道這么多,你知道 哨兵集群原理么?
哨兵是 Redis 的一種運(yùn)行模式,它專(zhuā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哨兵集群
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 同步;并且通知客戶(hù)端與新 master 建立連接。
面試官:哨兵之間是如何知道彼此的?
哨兵與 master 建立通信,利用 master 提供發(fā)布/訂閱機(jī)制發(fā)布自己的信息,比如身高體重、是否單身、IP、端口……
master 有一個(gè) __sentinel__:hello 的專(zhuān)用通道,用于哨兵之間發(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)控哨兵。
INFO命令獲取slave信息
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í)別的用戶(hù)規(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ù)多少可能不一樣。
Redis 集群架構(gòu)
三個(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)系如下:
數(shù)據(jù)、Slot與實(shí)例的映射
面試官: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)為確定下線(xiàn)狀態(tài) (Fail),然后向整個(gè)集群廣播,強(qiáng)迫其它節(jié)點(diǎn)也接收該節(jié)點(diǎn)已經(jīng)下線(xiàn)的事實(shí),并立即對(duì)該失聯(lián)節(jié)點(diǎn)進(jìn)行主從切換。
面試官:客戶(hù)端又怎么確定訪(fǎng)問(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)客戶(hù)端連接任何一個(gè)實(shí)例,實(shí)例就將哈希槽與實(shí)例的映射關(guān)系響應(yīng)給客戶(hù)端,客戶(hù)端就會(huì)將哈希槽與實(shí)例映射信息緩存在本地。
當(dāng)客戶(hù)端請(qǐng)求時(shí),會(huì)計(jì)算出鍵所對(duì)應(yīng)的哈希槽,再通過(guò)本地緩存的哈希槽實(shí)例映射信息定位到數(shù)據(jù)所在實(shí)例上,再將請(qǐng)求發(fā)送給對(duì)應(yīng)的實(shí)例。
Redis 客戶(hù)端定位數(shù)據(jù)所在節(jié)點(diǎn)
面試官:什么是 Redis 重定向機(jī)制?
哈希槽與實(shí)例之間的映射關(guān)系由于新增實(shí)例或者負(fù)載均衡重新分配導(dǎo)致改變了,客戶(hù)端將請(qǐng)求發(fā)送到實(shí)例上,這個(gè)實(shí)例沒(méi)有相應(yīng)的數(shù)據(jù),該 Redis 實(shí)例會(huì)告訴客戶(hù)端將請(qǐng)求發(fā)送到其他的實(shí)例上。
Redis 通過(guò) MOVED 錯(cuò)誤和 ASK 錯(cuò)誤告訴客戶(hù)端。
MOVED
MOVED 錯(cuò)誤(負(fù)載均衡,數(shù)據(jù)已經(jīng)遷移到其他實(shí)例上):當(dāng)客戶(hù)端將一個(gè)鍵值對(duì)操作請(qǐng)求發(fā)送給某個(gè)實(shí)例,而這個(gè)鍵所在的槽并非由自己負(fù)責(zé)的時(shí)候,該實(shí)例會(huì)返回一個(gè) MOVED 錯(cuò)誤指引轉(zhuǎn)向正在負(fù)責(zé)該槽的節(jié)點(diǎn)。
同時(shí),客戶(hù)端還會(huì)更新本地緩存,將該 slot 與 Redis 實(shí)例對(duì)應(yīng)關(guān)系更新正確。
MOVED 指令
ASK
如果某個(gè) slot 的數(shù)據(jù)比較多,部分遷移到新實(shí)例,還有一部分沒(méi)有遷移。
如果請(qǐng)求的 key 在當(dāng)前節(jié)點(diǎn)找到就直接執(zhí)行命令,否則時(shí)候就需要 ASK 錯(cuò)誤響應(yīng)了。
槽部分遷移未完成的情況下,如果需要訪(fǎng)問(wèn)的 key 所在 Slot 正在從 實(shí)例 1 遷移到 實(shí)例 2(如果 key 已經(jīng)不在實(shí)例 1),實(shí)例 1 會(huì)返回客戶(hù)端一條 ASK 報(bào)錯(cuò)信息:客戶(hù)端請(qǐng)求的 key 所在的哈希槽正在遷移到實(shí)例 2 上,你先給實(shí)例 2 發(fā)送一個(gè) ASKING 命令,接著發(fā)發(fā)送操作命令。
比如客戶(hù)端請(qǐng)求定位到 key = 「公眾號(hào):碼哥字節(jié)」的槽 16330 在實(shí)例 172.17.18.1 上,節(jié)點(diǎn) 1 如果找得到就直接執(zhí)行命令,否則響應(yīng) ASK 錯(cuò)誤信息,并指引客戶(hù)端轉(zhuǎn)向正在遷移的目標(biāo)節(jié)點(diǎn) 172.17.18.2。
ASK 錯(cuò)誤
注意:ASK 錯(cuò)誤指令并不會(huì)更新客戶(hù)端緩存的哈希槽分配信息。
未完待續(xù)
本篇主要將 Redis 核心內(nèi)容過(guò)了一遍,涉及到數(shù)據(jù)結(jié)構(gòu)、內(nèi)存模型、 IO 模型、持久化 RDB 和AOF 、主從復(fù)制原理、哨兵原理、cluster 原理。
「面霸」系列會(huì)分為幾篇,分別從核心原理、高可用、實(shí)戰(zhàn)、如何避坑等方面全方位拿下 Redis。
本文轉(zhuǎn)載自微信公眾號(hào)「 碼哥字節(jié)」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系 碼哥字節(jié)公眾號(hào)。