《redis設計與實現》讀書筆記

目錄

一、數據結構

1、字典

2、跳躍表

3、整數集合

4、壓縮列表

5、對象

二、單機數據庫

1、過期鍵刪除策略

2、RDB持久化

3、AOF持久化

4、事件

5、客戶端

6、服務端

三、多機數據庫

1、複製

2、Sentinel

3、集羣


一、數據結構

       主要記錄幾個比較獨特的數據結構。

1、字典

       整個Redis的鍵值對存儲可以說就是兩個哈希表;鍵衝突解決採用的開鏈,新節點總是在鏈表的頭部;計算鍵哈希值使用的是MurmurHash2算法。

       Redis的哈希表比較厲害的是他的rehash方法,因爲整個數據庫的數據量一般都會很大,一次rehash完肯定會阻塞數據庫很長一段時間,所以其rehash使用的漸進式rehash:

(1)首先,哈希表中有兩個數組ht[0]和ht[1],其中ht[0]爲平時使用的哈希表。如果是擴展操作,ht[1]的大小會擴展成第一個大於等於ht[0].used*2的2^n。如果是收縮操作,那麼ht[1]的大小爲第一個大於等於ht[0].used的2^n。

(2)在字典中維持了一個計數器rehashidx,並將它設爲0,表示開始rehash。

(3)每次對字典執行添加、刪除、查找或者更新操作時,還會順便將ht[0]在rehashidx索引上的所有鍵值對rehash到ht[1]上面,然後++rehashidx。

(4)最後所有鍵值對都遷移完畢後,將ht[0]設置爲ht[1],將ht[1]設置成ht[0],然後將rehashidx設爲-1,表示rehash完畢。

 

擴展時機:

       服務器沒有執行BGSAVE命令或者BGREWRITEAOF命令時負載因子大於等於1時,反之負載因子大於等於5時,執行擴展操作。原因是這兩個命令創建了子進程,而大多數操作系統都採用copy on write,所以創建了子進程後儘量避免不必要的寫操作。

收縮時機:

       當哈希表的負載因子小於0.1時執行收縮操作。

 

2、跳躍表

       Redis的跳躍表節點包含後退指針,分值(可以相同),成員對象(必須不同),和層。每次創建一個新的跳躍表節點時候,都根據冪次定律隨機生成一個介於1和32之間的值作爲level(層)數組的大小(levelDB的跳錶最大是12層,和這裏不同),層又包含前進指針和跨度。

 

3、整數集合

       整數集合保證集合中不會出現重複元素,整數集合中的整數有int16,int32,int64三種編碼方式,爲了節省內存,會優先使用最小的編碼方式,當插入了當前編碼方式不能接收的整數時會進行升級,將整個整數集合中所有數據升級成更大的編碼方式。目前不支持降級。

 

4、壓縮列表

       Redis中使用得非常多的數據結構,給我的震撼程度甚至比漸進式rehash還要大。

       一個壓縮列表節點主要由previous_entry_length,encoding,content組成。

       previous_entry_length:前一個entry的長度,如果前一節點長度小於254字節,那麼該屬性爲1字節。如果前一字節長度大於等於254字節,該屬性爲5字節,其中第一個字節爲0xFE(254),後四字節爲長度。

       encoding:保存數據的類型和長度。

       content:內容。

       壓縮列表存在一個問題,就是連鎖更新:前面一個entry的長度發生了變化,導致後面一連串的長度發送了變化,從而導致多次重新分配內存。

 

5、對象

(1)REDIS_STRING中的EMBSTR和RAW

       字符串長度小於等於39字節使用EMBSTR,否則使用RAW。raw編碼會調用兩次內存分配函數來分別創建redisObject結構和sdshdr結構,embstr則一次分配連續空間。

 

(2)REDIS_LIST中的ZIPLIST和LINKEDLIST

       若列表對象中保存的所有字符串元素的長度都小於64字節,且元素數量小於512個,則使用ziplist,否則使用linkedlist編碼。

 

(3)REDIS_HASH中的ZIPLIST和HT

       若哈希對象中保存的所有鍵或者值的字符串元素的長度都小於64字節,且元素數量小於512個,則使用ziplist,否則使用hashtable編碼。

       這裏ziplist存儲鍵值對的方式如下:

 

(4)REDIS_SET中的INTSET和HT

       集合對象全是整數,元素數量不超過512個的時候使用intset,否則使用hashtable。

使用hashtable編碼的集合對象如下,value值爲NULL:

                       

 

(5)REDIS_ZSET中的ZIPLIST和SKIPLIST

       有序集合保存的元素數量小於128個且所有元素的長度都小於64字節時使用ziplist,否則使用skiplist。

這裏使用skiplist實際還使用了一個字典存儲字符串和分值的映射:

       字符串使用的是指針,並沒有拷貝一份,所以沒有造成內存浪費。

 

二、單機數據庫

1、過期鍵刪除策略

       首先,上面也提到了,整個Redis的鍵值對存儲可以說就是兩個哈希表,其中一個存的是鍵值對,一個存的是鍵——過期時間。有了這個字典,redis每次就可以找到過期的鍵值對了。

       Redis採用的是惰性刪除和定期刪除兩種策略:

       惰性刪除就是執行操作前判斷一下當前鍵有沒有過期,過期了就將其刪除。

       定期刪除就是每當服務器週期性執行serverCron函數時,就會在規定時間內,分多次便利服務器中的各個數據庫,從數據庫中的過期字典中檢查一部分鍵的過期時間,並刪除其中的過期鍵。

 

2、RDB持久化

       執行SAVE或BGSAVE(創建子進程進行寫RDB文件)命令可以創建一個新的RDB文件,保存數據庫中所有的鍵值對,其中已過期的鍵不會被保存在新的RDB文件中。

       若以主服務器模式運行載入RDB時,過期鍵會被忽略,從服務器則都會載入。

       可以配置多個自動執行RDB的條件:n秒內對數據庫進行了至少m次修改則執行BGSAVE命令。主要實現就是有兩個變量dirty和lastsave,dirty記錄距離上次成功執行SAVE或BGSAVE命令之後數據庫進行了多少次的修改;lastsave記錄上次SAVE或BGSAVE的時間。然後服務器每隔一定時間(默認100ms),將這兩個變量與參數數組(記錄了配置值)進行比較,,滿足配置條件就BGSAVE。

       RDB文件結構如下:

       其中的database部分結構如下:

                                 

       其中key_value_pairs的結構如下:

                                                                     

                                 

       當開啓了RDB文件壓縮功能的情況下,對於字符串長度大於20字節的對象會被壓縮後再保存(使用的LZF算法),對於ziplist也會先轉換成一個字符串對象,再壓縮保存。

 

3、AOF持久化

       AOF文件實際上就是將每一條指令都記錄了下來(可以設置爲每條指令來了都flush到磁盤也可以設置爲定期flush到磁盤),使用AOF文件載入還原數據也就是相當於重新輸指令了。那麼這就可能產生很多冗餘數據,所以redis提供AOF文件重寫功能BGREWRITEAOF,該功能可以理解成用指令記錄數據庫目前的鍵值對,這樣一個鍵值對就只對應一條指令了。

       那麼,子進程進行AOF重寫期間新的命令對現有的數據庫狀態進行了修改怎麼辦呢?Redis設置了一個AOF重寫緩衝區,會記錄AOF重寫期間的寫命令,最後追加到新的AOF文件中。

 

4、事件

       Redis服務器是一個事件驅動程序,包含文件事件和時間事件,使用的reactor模型。其中時間時間分爲定時事件和週期性事件,實現方法是將所有時間事件放在一個無序鏈表中,每當時間事件執行器運行時,就遍歷整個鏈表,查找已到達的時間事件,並調用相應的事件處理器。

 

5、客戶端

       服務器使用clients鏈表連接起多個客戶端狀態,用flags屬性表示客戶端角色和狀態。服務器中一個客戶端對象包含一個輸入緩衝區和一個輸出緩衝區:輸入緩衝區記錄了客戶端發送的命令請求,不能超過1GB,命令參數記錄在argc和argc中;輸出緩衝區分爲固定大小和可變大小兩種,固定大小的最大爲16KB,可變大小的不能超過服務器設置的硬性限制,否則立刻關閉該客戶端,另外,日過一定時間內一直超過服務器設置的軟性限制,那麼客戶端也會被關閉。

 

6、服務端

       主要是一個serverCron函數,該函數默認每隔100毫秒執行一次,它的工作主要包括更新服務器狀態信息,處理服務器接收的SIGTERM信號(先進行持久化操作,在關閉服務器),管理客戶端資源和數據庫狀態,檢查執行持久化操作。

 

三、多機數據庫

1、複製

       Redis的複製功能主要分爲命令傳播和同步(sync)。

       命令傳播:

       主服務器接收到的寫命令會發送給從服務器。

 

       同步PSYNC功能,分爲完全重同步和部分重同步:

       完全重同步:主服務器執行BGSAVE命令,並記錄從此刻起的所有寫命令。然後將生成的RDB文件和記錄的寫命令發送給從服務器,即完成同步。

       部分重同步:用於斷線處理,即只發送斷線期間的寫命令。

 

       部分重同步的實現主要就是依賴一個複製偏移量,一個複製積壓緩衝區,主從服務器偏移量不同則說明發生了斷線,則根據複製擠壓緩衝區內容恢復從服務器,若複製積壓緩衝區數據不夠就進行完全重同步。另外,從服務器是會保存主服務器id的,若斷線後發現主服務器的id和當前保存的主服務器id一致才進行部分重同步。

 

2、Sentinel

       由一個或多個Sentinel實例組成的Sentinel系統可以監視任意多個主服務器,在主服務器下線時,自動將下線主服務器屬下的某個從服務器升級爲新的主服務器。

       首先,sentinel會想主服務器建立兩條連接(一條用於發送命令,一條用於訂閱頻道),然後默認會以十秒一次的頻率通過命令連接想監視的主服務器發送INFO命令獲取主服務器的信息來更新主服務器實例對象。也會默認以十秒一次的頻率想從服務器發送INFO命令更新從服務器實例對象。不僅如此,sentinel還會爲同樣監視這個主服務器的其它sentinel創建一個字典來保存其它sentinel的資料,並與之創建命令連接(沒有訂閱連接)。

       默認情況下,seninel會以每秒一次的頻率向所有與它創建了連接的實例發送PING命令來判斷是否下線。當足夠多的sentinel判定主服務器主觀下線(各sentinel可能有不同的配置,比如有的sentinel認爲30000ms爲下線標準而有的認爲5000ms)後,就認爲其客觀下線。

當一個主服務器被判斷爲客觀下線後,監視這個主服務器的各個sentinel就會進行協商,選出一個領頭Sentinel(Raft共識算法的一個實現):

(1)所有sentinel平等。

(2)最先向目標sentinel發送設置要求的源sentinel將成爲目標sentinel的局部領頭sentinel(也就是先到先得,先到先得也就說明了自己的網絡狀態相對來說是比較好的),之後的設置要求都會被忽略。

(3)一定時間內,如果有某個sentinel被半數以上的sentinel設置成局部領頭,那這個sentinel就成爲領頭sentinel,進行故障轉移操作。若沒有,則重新進行選舉。

       最後,則進行故障轉移,即挑選一個從服務器爲主服務器,然後將已下線的主服務器設置爲新的主服務器的從服務器。篩選方法如下:

(1)刪除下線的從服務器。

(2)刪除最近五秒沒有回覆過INFO命令的從服務器。

(3)刪除所有與已下線主服務器連接斷開超過down-after-milliseconds * 10毫秒的從服務器,即保證剩餘的從服務器保存的數據都比較新。

(4)根據優先級排序,挑選第一個從服務器。

 

3、集羣

       通過CLUSTER MEET加入集羣(使用的Gossip協議傳播給集羣其它節點,簡單來說就是每秒都會隨機向其他節點發送自己所擁有的節點列表,以及需要傳播的消息。任何新加入的節點,就在這種傳播方式下很快地被全網所知道)。

       Redis集羣通過分片的方式來保存數據庫中的鍵值對:集羣的整個數據庫被分爲16384個槽,每個鍵都屬於這16384個槽的其中一個,集羣中的每個節點可以處理0到16384個槽。

       每個節點如何記錄自己處理哪些槽呢,使用的是二進制數(bitmap)。同時還會保存集羣所有槽的指派信息,即集羣中的每個節點都會知道數據庫中的16384個槽分別被指派給了集羣中的哪些節點。

       當客戶端向節點發送與數據庫鍵有關的命令時,接收命令的節點會計算出命令要處理的數據庫屬於哪個槽,並檢查這個槽是否指派給了自己,若沒有指派給自己,就會像客戶端返回一個MOVED錯誤,指引客戶端轉向正確的節點。

       節點計算鍵的CRC-16校驗和然後&16383即可得到鍵所屬槽號。

       當需要重新分片(將任意數量已經指派給某個節點的槽改爲指派給另一節點)時,由redis-trib負責執行,主要就是命令源節點原子地將鍵值對遷移到目標節點。這時如果客戶端發送命令,源節點會首先從自己的數據庫找指定的鍵,若沒有則返回ASK錯誤引導客戶端轉向正確的節點。這裏ASK和MOVED錯誤的區別在於,產生MOVED錯誤後,客戶端之後都會將命令發送至修改後的目標節點,而產生ASK命令後,客戶端今後還是會發送到之前發送的節點,因爲其它的鍵還是有可能存在源節點的。

       那麼,如何處理節點的異常下線呢,實際和sentinel的處理方式很像。首先半數以上負責處理槽的主節點都將某個主節點x報告爲疑似下線後,這個主節點就將被標記爲已下線(FAIL),然後標記爲已下線的節點會向集羣廣播一條關於主節點x的FAIL消息,所有收到這條消息的節點都將立即標記x已下線。接下來就是選舉新的主節點,方法和選舉sentinel頭領類似,當從節點發現自己的主節點下線,會向集羣廣播一條消息,其它主節點會票選自己第一個收到消息的從節點,某個從節點獲得N/2 + 1張投票時它成爲主節點(其實這也說明它的網絡狀況最好)。

 

參考:《Redis設計與實現》

發佈了13 篇原創文章 · 獲贊 3 · 訪問量 1989
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章