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