3 萬字聊聊什么是 Redis
大家好,我是Leo。
結(jié)束了漫長了MySQL,開始步入了Redis的殿堂。最近在做Redis技術(shù)輸出時(shí),明顯發(fā)現(xiàn)進(jìn)一步熟悉MySQL之后,對(duì)Redis的理解容易了許多?;蛟S這就是進(jìn)步吧!
下面的思路部分,可以幫助你更好的理解這篇文章的知識(shí)體系。
思路
整體結(jié)構(gòu)
Redis主要是由訪問框架,操作模塊,索引模塊,存儲(chǔ)模塊,高可用集群支撐模塊,高可用擴(kuò)展支撐模塊等組成,
Redis還有一些,豐富的數(shù)據(jù)類型,數(shù)據(jù)壓縮,過期機(jī)制,數(shù)據(jù)淘汰策略,分片機(jī)制,哨兵模式,主從復(fù)制,集群化,高可用,統(tǒng)計(jì)模塊,通知模塊,調(diào)試模塊,元數(shù)據(jù)查詢等輔助功能。
接下來的Redis學(xué)習(xí)之路,主要是圍繞介紹上述模塊,功能,策略,機(jī)制,算法等知識(shí)的輸出。
五大類型
String
String類型應(yīng)該是我們用的最多的一種類型,它的底層是由簡(jiǎn)單的動(dòng)態(tài)字符串實(shí)現(xiàn)的。
hash
hash類型也是我們用的最多的一種類型了,它是由壓縮列表+哈希表共同實(shí)現(xiàn)的一種數(shù)據(jù)類型
list
list它是一種列表類型,也是我們常用類型之一,它是由雙向鏈表+壓縮列表共同實(shí)現(xiàn)的一種數(shù)據(jù)類型
set
set集合和上述類型不同,他不允許重復(fù),所以一些特定的場(chǎng)景會(huì)優(yōu)先考慮set類型,它是由整數(shù)數(shù)組+哈希表共同實(shí)現(xiàn)的一種數(shù)據(jù)類型
sort set
sortset是在set的基礎(chǔ)上,做的一個(gè)提升,不允許重復(fù)的時(shí)候,還可以處理有序。主要應(yīng)用與排序表之類的場(chǎng)景需求,它是由壓縮鏈表+跳表實(shí)現(xiàn)的一種數(shù)據(jù)類型
數(shù)據(jù)結(jié)構(gòu)
哈希表會(huì)在下文rehash那里詳細(xì)介紹一下。
整數(shù)數(shù)組和雙向鏈表也很常見,它們的操作特征都是順序讀寫,也就是通過數(shù)組下標(biāo)或者鏈表的指針逐個(gè)元素訪問,操作復(fù)雜度基本是 O(N),操作效率比較低。
壓縮列表實(shí)際上類似于一個(gè)數(shù)組,數(shù)組中的每一個(gè)元素都對(duì)應(yīng)保存一個(gè)數(shù)據(jù)。和數(shù)組不同的是,壓縮列表在表頭有三個(gè)字段 zlbytes、zltail 和 zllen,分別表示列表長度、列表尾的偏移量和列表中的 entry 個(gè)數(shù);壓縮列表在表尾還有一個(gè) zlend,表示列表結(jié)束。在壓縮列表中,如果我們要查找定位第一個(gè)元素和最后一個(gè)元素,可以通過表頭三個(gè)字段的長度直接定位,復(fù)雜度是 O(1)。而查找其他元素時(shí),就沒有這么高效了,只能逐個(gè)查找,此時(shí)的復(fù)雜度就是 O(N) 了。
跳表在鏈表的基礎(chǔ)上,增加了多級(jí)索引,通過索引位置的幾個(gè)跳轉(zhuǎn),實(shí)現(xiàn)數(shù)據(jù)的快速定位。在下述文章中的第五章節(jié)介紹過了跳表的相關(guān)說明。
哈希為啥變慢了
Redis在處理一個(gè)鍵值對(duì)時(shí),會(huì)進(jìn)行一次hash處理,把鍵處理成一個(gè)地址碼寫入Redis的存儲(chǔ)模塊,隨著我們key的越來越多,有一些key會(huì)存在同一個(gè)地址碼的情況。(我在寫hashmap的時(shí)候就介紹過hash碰撞的問題)
出現(xiàn)這種情況之后Redis作了一個(gè)鍵值對(duì)的擴(kuò)展,也就是鍵值對(duì)+鏈表的方式。如下圖,多個(gè)數(shù)據(jù)經(jīng)過hash處理之后,都落到了key1值上。一個(gè)卡槽不可能存放兩個(gè)值,于是就在這個(gè)卡槽存了指向一個(gè)鏈表的指針,通過鏈表存儲(chǔ)多個(gè)值。
哈希鏈表
鏈表處理的就是多個(gè)key一樣的問題,隨著數(shù)據(jù)量的發(fā)展,哈希碰撞的情況越來越頻繁,鏈表的數(shù)據(jù)也就越來越多。hash的性能是O(1),鏈表的性能是O(n)。所以整體的性能被拖下來了。為了改變這一現(xiàn)狀,Redis引入了rehash。
rehash
rehash就是增加現(xiàn)有的哈希桶的數(shù)量,讓逐漸增多的元素能在更多的哈希桶之間分散保存。從而減少單個(gè)桶的鏈表的元素?cái)?shù)量,同時(shí)也減少單個(gè)桶的沖突。
首先Redis會(huì)先創(chuàng)建兩個(gè)全局哈希表,我們這里定義為哈希表A,哈希表B。我們?cè)诓迦胍粋€(gè)數(shù)據(jù)時(shí),先先存入A,隨著A越來越多,Redis開始執(zhí)行rehash操作。主要分為三步:
- 給B分配更多的空間,一般都是A的兩倍
- 把A中的數(shù)據(jù)全部拷貝到B中
- 釋放A
上述rehash流程我們可以看出,當(dāng)A中存在大量的數(shù)據(jù),拷貝的效率是非常慢的!因?yàn)镽edis的單線程性還會(huì)造成阻塞,導(dǎo)致Redis短時(shí)間無法提供服務(wù)。為了避免這一問題,Redis在rehash的基礎(chǔ)上,采用了漸進(jìn)式rehash。
漸進(jìn)式 rehash
進(jìn)化點(diǎn)就是在第二步拷貝的時(shí)候,并不是一次性拷貝的,而是分批次拷貝。在處理一個(gè)請(qǐng)求時(shí),從A中的第一個(gè)索引位置開始,順帶著將這個(gè)索引位置上的所有元素拷貝到B中。等下一個(gè)請(qǐng)求后,再從A表中的下一個(gè)索引位置繼續(xù)拷貝操作。這樣就巧妙地把一次性大量拷貝的開銷,分?jǐn)偟搅硕啻翁幚碚?qǐng)求的過程中,避免了耗時(shí)操作,保證了數(shù)據(jù)的快速訪問。
Redis單線程還是多線程
先來普及一下多線程的知識(shí),一個(gè)CPU在運(yùn)行多個(gè)線程時(shí),會(huì)有一個(gè)多線程調(diào)用的消耗問題,而且還有多個(gè)線程調(diào)用時(shí)數(shù)據(jù)一致性的問題。這些都要單獨(dú)處理,單獨(dú)處理又會(huì)消耗性能。于是Redis統(tǒng)籌兼顧采用了單,多線程并用的思路。
在處理數(shù)據(jù)寫入,讀取屬于鍵值對(duì)數(shù)據(jù)操作,采用單線程操作。在請(qǐng)求連接,從socket中讀取請(qǐng)求,解析客戶端發(fā)送請(qǐng)求,采用多線程操作。
Redis巧妙的把所有需要延遲等待的操作全部轉(zhuǎn)交給了多線程處理,在不需要等待的全部單線程處理。個(gè)人感覺這種設(shè)計(jì)思路很棒
tip:如果不按照這種方式設(shè)計(jì)的,連接之后等待,發(fā)送等待,接收等待估計(jì)要等死你哦。造成Redis線程阻塞,無法處理其他請(qǐng)求。
多路復(fù)用機(jī)制
IO多路復(fù)用機(jī)制是指一個(gè)線程處理多個(gè)IO流,也是我們經(jīng)常聽到的select/epoll機(jī)制。那么那些連接,等待的操作Redis都是如何處理的呢?
在Redis只運(yùn)行單線程的情況下,同一時(shí)間存在多個(gè)監(jiān)聽套接字,和已連接的套接字,內(nèi)核會(huì)一直監(jiān)聽這些連接請(qǐng)求和數(shù)據(jù)請(qǐng)求。一旦客戶端發(fā)送請(qǐng)求就會(huì)以事件的方式通知Redis主線程處理。這就是Redis線程處理多個(gè)IO流的效果。
上文說到以事件方式通知Redis這里我們做一個(gè)擴(kuò)展,select/epoll提供了基于事件的回調(diào)機(jī)制,不同的事件會(huì)調(diào)用相應(yīng)的處理函數(shù)。一旦請(qǐng)求來了,立刻加到事件隊(duì)列中,Redis單線程就會(huì)源源不斷的處理該事件隊(duì)列。解決了等待與掃描的資源浪費(fèi)問題。
安全機(jī)制
Redis的持久化安全機(jī)制主要有兩大塊,一塊是AOF日志,一塊是RDB快照,接下來我們聊聊AOF與RDB的一些區(qū)別吧
AOF
Redis為了提升性能采用的是寫后日志,先執(zhí)行命令,后寫日志,這樣做的好處主要有兩點(diǎn)
- 只有當(dāng)命令執(zhí)行成功之后才會(huì)寫入日志。這樣就避免了寫入日志之后,命令執(zhí)行錯(cuò)誤還要把日志刪掉的問題。
- 先執(zhí)行寫入操作,后寫日志,這樣同時(shí)也避免了阻塞當(dāng)前的寫操作
壞處是:
- 如果一個(gè)命令執(zhí)行完后,還沒記錄日志就宕機(jī)了,那么這個(gè)命令和相應(yīng)的數(shù)據(jù)就有丟失的風(fēng)險(xiǎn)。
- AOF雖然避免了對(duì)當(dāng)前命令的阻塞,但可能會(huì)對(duì)下一個(gè)操作帶來阻塞風(fēng)險(xiǎn)。因?yàn)锳OF日志也是在主線程中執(zhí)行的,并且是
- 寫入磁盤。
文件格式:
Redis收到一個(gè) "set huanshao 公眾號(hào)歡少的成長之路" 命令后,AOF的日志內(nèi)容是,"*3" 表示當(dāng)前命令有三個(gè)部分,每部分都是由
+數(shù)字”開頭,后面緊跟著具體的命令、鍵或值。這里,“數(shù)字”表示這部分中的命令、鍵或值一共有多少字節(jié)。例如,“
3 set”表示這部分有 3 個(gè)字節(jié),也就是“set”命令。
AOF寫入策略
AOF提供了三種appendfsync可選值
- Always,同步寫回:每個(gè)寫命令執(zhí)行完,立馬同步地將日志寫回磁盤;
- Everysec,每秒寫回:每個(gè)寫命令執(zhí)行完,只是先把日志寫到 AOF 文件的內(nèi)存緩沖區(qū),每隔一秒把緩沖區(qū)中的內(nèi)容寫入磁盤;
- No,操作系統(tǒng)控制的寫回:每個(gè)寫命令執(zhí)行完,只是先把日志寫到 AOF 文件的內(nèi)存緩沖區(qū),由操作系統(tǒng)決定何時(shí)將緩沖區(qū)內(nèi)容寫回磁盤。
這三種都無法做到兩全其美,同步寫會(huì)可以做到數(shù)據(jù)一致性,但是寫入磁盤的這個(gè)性能對(duì)比內(nèi)存來說太差了,如果是每秒寫的話,就會(huì)丟失1秒的數(shù)據(jù),如果No配置的話宕機(jī)后丟失數(shù)據(jù)比較多。
最后三種配置如何選擇,應(yīng)該根據(jù)特定的業(yè)務(wù)場(chǎng)景。如果數(shù)據(jù)安全性過高就選擇同步寫回,如果適中就每秒寫回,沒安全性的話就選擇No。
AOF重寫機(jī)制
AOF日志是追加形式的,避免不了的就是文件過大之后,再寫入日志的性能會(huì)有所下降,Redis為了解決這一難題,引入了重寫機(jī)制。
重寫機(jī)制主要做的事情是記錄一個(gè)key值的最終修改結(jié)果,修改的歷史記錄一律排除。這樣一來,一個(gè)命令就只有一個(gè)日志。如果要拿AOF日志恢復(fù)數(shù)據(jù)的話也能恢復(fù)出正確的數(shù)據(jù)。
重寫機(jī)制流程就是主線程fork出一個(gè)后臺(tái)子線程 bgrewriteaof后,fork會(huì)把主線程的內(nèi)存拷貝一份給子線程bgrewriteaof,這樣子線程就可以在不影響主線程阻塞的情況下進(jìn)行重寫操作了。
在這段期間,如果有新的請(qǐng)求寫入過來,Redis會(huì)有兩個(gè)日志,一個(gè)日志指正在使用的 AOF 日志,Redis 會(huì)把這個(gè)操作寫到它的緩沖區(qū)。這樣一來,即使宕機(jī)了,這個(gè) AOF 日志的操作仍然是齊全的,可以用于恢復(fù)。另一處日志指新的 AOF 重寫日志。這個(gè)操作也會(huì)被寫到重寫日志的緩沖區(qū)。這樣,重寫日志也不會(huì)丟失最新的操作。等到拷貝數(shù)據(jù)的所有操作記錄重寫完成后,重寫日志記錄的這些最新操作也會(huì)寫入新的 AOF 文件,以保證數(shù)據(jù)庫最新狀態(tài)的記錄。此時(shí),我們就可以用新的 AOF 文件替代舊文件了。
RDB
RDB是一種內(nèi)存快照,它是系統(tǒng)某一刻的數(shù)據(jù)備份寫到磁盤上。這樣就可以達(dá)到宕機(jī)后,可以恢復(fù)某一刻之前的所有數(shù)據(jù)。
生成RDB的兩種方式
- save:在主線程中執(zhí)行,會(huì)導(dǎo)致阻塞;
- bgsave:創(chuàng)建一個(gè)子進(jìn)程,專門用于寫入 RDB 文件,避免了主線程的阻塞,這也是 Redis RDB 文件生成的 默認(rèn)配置。
寫時(shí)復(fù)制技術(shù)
首先介紹一下寫時(shí)復(fù)制技術(shù)的由來,在Redis做RDB快照時(shí)(當(dāng)前RDB還沒有做完),來了一個(gè)修改數(shù)據(jù)的請(qǐng)求。如果把這個(gè)請(qǐng)求寫入快照,那么就不符合那一刻的數(shù)據(jù)一致性。如果不寫入快照把他丟棄,就會(huì)造成數(shù)據(jù)丟失還是會(huì)有數(shù)據(jù)一致性的問題。所以Redis借助操作系統(tǒng)提供的寫時(shí)復(fù)制技術(shù),在執(zhí)行快照的同時(shí),正常處理寫操作。
處理流程
主線程fork創(chuàng)建子線程bgsave,可以共享主線程的所有內(nèi)存數(shù)據(jù),bgsave子線程運(yùn)行后,開始讀取主線程的內(nèi)存數(shù)據(jù),并把它們寫入 RDB 文件。如果主線程對(duì)這些數(shù)據(jù)都是讀操作,那么互不影響。如果是修改操作的話就會(huì)把這塊數(shù)據(jù)復(fù)制一份,生成該數(shù)據(jù)的副本。然后主線程在這個(gè)副本上進(jìn)行修改。同時(shí)bgsave 子進(jìn)程可以繼續(xù)把原來的數(shù)據(jù)寫入 RDB 文件。
這樣保證了快照的數(shù)據(jù)一致性,也保證了快照期間對(duì)正常業(yè)務(wù)的影響。
既然RDB那么牛逼,可否用RDB做持久化呢?
如果我們采用RDB做持久化的話,那么就要一直進(jìn)行RDB快照,如果每2秒做一次快照的話,最壞的打算就要少50%的數(shù)據(jù)量,如果每秒做一次快照,可以完全保證數(shù)據(jù)的一致性但是帶來的負(fù)面影響也是非常大的。
- 頻繁快照,導(dǎo)致磁盤IO占用影響,且磁盤內(nèi)存開銷非常大
- RDB由bgsave處理,雖然不阻塞主線程,但是主線程新建bgsave時(shí),會(huì)影響主線程,如果每秒新建一次,有可能會(huì)阻塞主線程的。
全量備份不行的話,增量備份是否可以用RDB做持久化呢?
增量備份與全量備份的區(qū)別就是,增量備份只備份修改的數(shù)據(jù)。如果是這樣的話,我們就需要對(duì)每一個(gè)數(shù)據(jù)都加一個(gè)記錄,這樣開銷是十分大的。如果為了增量備份犧牲了寶貴的內(nèi)存資源,這就有點(diǎn)得不償失了。
實(shí)戰(zhàn)應(yīng)用
上述我們介紹了AOF與RDB的區(qū)別,流程,優(yōu)缺點(diǎn)。我們可以發(fā)現(xiàn),如果只依靠某一種方式進(jìn)行持久化都無法有效的達(dá)到數(shù)據(jù)一致性。
如果只用RDB,快照的頻率不好把握,如果使用AOF,文件持續(xù)變大也是吃不消的。
最優(yōu)的策略就是 RDB + AOF 假如每小時(shí)備份一次RDB,我們就可以利用RDB文件恢復(fù)那一刻的所有數(shù)據(jù),然后再用AOF日志恢復(fù)這一小時(shí)的數(shù)據(jù)。