Redis設計原理


1.簡介

Redis中的每個Key-Value在內存中都會被划分成DictEntry以及代表Key和Value的對象。

DictEntry包含分別指向Key和Value對象的指針以及指向下一個DictEntry的指針。

Redis使用RedisObject來表示對象,由於Key固定是字符串類型,因此使用字符串對象來表示,Value可以是字符串、列表、哈希、集合、有序集合對象中的一種。

 

 

 

Redis使用redisObject結構來表示對象(存儲對象的相關信息)

typedef struct redisObject { unsigned type; unsigned encoding; unsigned lru; int refcount; void *ptr; }robj;
type屬性:存儲對象的類型(String、List、Hash、Set、ZSet中的一種)

encoding屬性:存儲對象使用的編碼方式,不同的編碼方式使用不同的數據結構進行存儲。

lru屬性:存儲對象最后一次被訪問的時間。

refcount屬性:存儲對象被引用的次數。

*ptr指針:指向對象的地址。

 

使用type命令可以查看對象的類型。

使用object encoding命令可以查看對象使用的編碼方式。

使用object idletime命令可以查看對象的空閑時間(即多久沒有被訪問)

使用object refcount命令可以查看對象被引用的次數。

*這些命令都是通過Key找到對應的DictEntry,再從DictEntry的*value指針所指的RedisObject中進行獲取。

 

 

2.字符串

 

 

Redis使用sdshdr結構來表示字符串對象,並沒有直接使用C語言的字符串。

struct sdshdr { int len; int free; char buf[]; };
len屬性:存儲字符串的長度。

free屬性:存儲字節數組中未使用的字節數量。

buf[]屬性:字節數組,用於存儲字符。

*字節數組中會有\0結束符,該結束符不會記錄在len屬性中。

 

SDS相比C語言的字符串

C語言中存儲字符串的字節數組其長度總是N+1(最后一個是結束符),當對字符串進行增長和縮短操作時需要使用內存重分配來重新為對象分配內存。

為了減少內存重分配的次數,Redis自定義了字符串對象(sdshdr),通過未使用的空間解除了字符串長度與底層數組長度之間的關系,在SDS中buf數組的長度不一定等於字符串的長度+1,數組里面還可以包含未使用的字節。

通過未使用的空間,SDS實現了空間預分配惰性空間釋放兩種策略,從而減少由於字符串的修改導致內存重分配的次數。

空間預分配:當需要對SDS保存的字符串進行增長操作時,程序除了會為SDS分配所必須的空間以外,還會為SDS分配額外的未使用的空間。

惰性空間釋放:當需要對SDS保存的字符串進行縮短操作時,程序並不會立即使用內存重分配來回收縮短后多出來的字節,而是使用free屬性將這些多出來的字節數量記錄出來,等待將來使用。

 

 

3.字典

 

字典是Redis數據庫以及HashTable編碼方式的底層實現。 

字典的底層使用散列,同時使用鏈地址法的方式解決散列沖突,那么最終就是指針數組的形式,數組中的每個元素都是一個指向DictEntry的指針。

 

 

Redis使用dictht結構來表示散列表

typedef struct dictht { dictEntry **table; unsigned long size; unsigned long sizemask; unsigned long used; }dictht;
table指針:指向散列表的地址。

size屬性:存儲散列表的大小。

sizemask屬性:用於計算索引值。

used屬性:散列表中節點的個數。

 

Redis使用dictEntry結構來表示散列表中的節點

typedef struct dictEntry { void *key; union{ void *val; uint_tu64; int64_ts64; }v struct dictEntry next*; }dictEntry; 
key指針:指向Key對象。

value屬性:可以是指向Value對象(指針)、uint64_t整數、int64_t整數。

next指針:指向下一個DictEntry。

 

Redis使用dict結構來表示字典,每個字典中包含兩個dictht。

typedef struct dict{ dictType *type; void *privatedata; dictht ht[2]; int rehashidx; }dict;
type指針:指向DictType,DictType定義了一系列函數。

privatadata屬性:傳給特定函數的可選參數。

ht數組:長度為2的dictht數組,一般情況下只會使用ht[0]散列表,ht[1]散列表只會在對ht[0]散列表進行rehash時使用。

rehashidx屬性:記錄rehash的進度,如果目前沒有進行rehash那么值為-1。

 

dictType的結構(定義了一系列函數)

typedef struct dictType{   
    unsigned int (*hashFunction)(const void *key); // H(K)散列函數
    void *(*keyDup)(void *privatedata, const void *key); // 復制Key
    void *(*valDup)(void *privatedata, const void *obj); // 復制Value    
    int (*keyCompare)(void *privatdata, const void *key1 , const void *key2); // 對比Key    
    void (*keyDestructor)(void *privatedata, void *key); // 銷毀Key    
    void (*valDestructor)(void *privatedata, void *obj); // 銷毀Value
}dictType;

 

3.1 在字典中進行查找、添加、更新、刪除操作

 

在字典中進行查找

以客戶端傳遞的Key作為關鍵字K,通過dict中的dictType的H(K)散列函數計算散列值,使用dictht[0]的sizemask屬性和散列值計算索引,然后遍歷索引對應的鏈表,如果存在Key相同的DictEntry則直接返回,否則返回NULL。

 

在字典中進行添加和更新

以客戶端傳遞的Key作為關鍵字K,通過dict中的dictType的H(K)散列函數計算散列值,使用dictht[0]的sizemask屬性和散列值計算索引,然后遍歷索引對應的鏈表,如果存在Key相同的DictEntry則進行更新,否則創建代表Key和Value的對象,然后創建一個DictEntry並使其分別指向Key和Value的對象,最終將該DictEntry追加到鏈表的末尾。

 

在字典中進行刪除(查到后進行刪除)

以客戶端傳遞的Key作為關鍵字K,通過dict中的dictType的H(K)散列函數計算散列值,使用dictht[0]的sizemask屬性和散列值計算索引,然后遍歷索引對應的鏈表,如果存在Key相同的DictEntry則進行刪除。

 

3.2 散列表的擴容和縮容

由於散列表的負載因子需要維持在一個合理的范圍內,因此當散列表中的元素過多時會進行擴容,過少時會進行縮容。

一旦散列表的長度發生改變,那么就要進行rehash,即對原先散列表中的元素在新的散列表中重新進行hash。

Redis中的rehash是漸進式的,並不是一次性完成,因為出於性能的考慮,如果散列表中包含上百萬個節點,如果一次性完成rehash的話,那么有可能導致Redis在一定時間內無法正常對外提供服務。

在rehash進行期間,每次對字典執行查找、添加、更新、刪除操作時,除了會執行相應的操作以外,還會順帶的將ht[0]散列表在rehashidx索引上的所有節點rehash到ht[1]上,然后將rehashidx屬性的值加1。

 

漸進式Rehash的步驟

1.為字典的ht[1]散列表分配空間。

*若執行的是擴容操作,那么ht[1]的長度為第一個大於等於ht[0].used*2的2ⁿ。 

*若執行的是縮容操作,那么ht[1]的長度為第一個大於等於ht[0].used的2ⁿ。

2.rehashidx屬性設置為0,表示開始進行rehash。

3.在rehash進行期間,每次對字典執行查找、添加、更新、刪除操作時,除了會執行相應的操作以外,還會順帶將ht[0]散列表在rehashidx索引上的所有節點rehash到ht[1]上,然后將rehashidx屬性的值加1。

4.隨着對字典的不斷操作,最終在某個時刻,ht[0]散列表中的所有節點都會被rehash到ht[1]上,此時將rehashidx屬性設置為-1,表示rehash已結束。

*在進行漸進式rehash的過程中,字典會同時使用ht[0]和ht[1]兩個散列表,因此字典的查找、更新、刪除操作會在兩個散列表中進行,如果在ht[0]計算得到的索引指向NULL則從ht[1]中進行查找。

 

 

4.Redis提供的編碼方式

 

Redis提供了八種編碼方式,每種編碼方式都有其特定的數據結構。

redis_encoding_int // 整數字符串

redis_encoding_embstr // 短字符串

redis_encoding_row // 長字符串

redis_encoding_ziplist // 壓縮列表

redis_encoding_linkedlist // 鏈表

redis_encoding_intset // 整數集合

redis_encoding_hashtable // hashTable

redis_encoding_skiplist // 跳躍表

 

1.INT編碼方式

 

INT編碼方式會將RedisObject中的*ptr指針直接改寫成long ptr,ptr屬性直接存儲字面量。

 

2.EMBSTR編碼方式

  

3.ROW編碼方式

 

 

*EMBSTR和ROW編碼方式在內存中都會創建字符串對象(SDS),區別在於EMBSTR編碼方式中RedisObject和SDS共同使用同一塊內存單元,Redis內存分配器只需要分配一次內存,而ROW編碼方式中需要單獨的為RedisObject和SDS分配內存單元。

 

4.ZIPLIST編碼方式

壓縮列表是Redis為了節約內存而開發的,它是一塊順序表(順序存儲結構,內存空間連續),一個壓縮列表中可以包含多個entry節點,每個entry節點可以保存一個整數值或者字符串。

zlbytes:記錄了壓縮列表的大小(占4個字節)

zltail:記錄了壓縮列表最后一個節點距離起始位置的大小(占4個字節)

zllen:記錄了壓縮列節點的個數(占2個字節)

entry:壓縮列表中的節點(大小由節點中存儲的內容所決定)

zlend:壓縮列表的結束標志(占1個字節)

 

如果存在一個指針P指向壓縮列表的起始位置,就可以根據P+zltail得到最后一個節點的地址。

 

5.LINKEDLIST編碼方式

 

Redis使用listNode結構來表示鏈表中的節點。

typedef struct listNode { struct listNode *prev; struct listNode *next; void *value; }listNode;

每個listNode節點分別包含指向前驅和后繼節點的指針以及指向元素的指針。

 

Redis使用list結構來持有listNode

typedef struct list { listNode *head; listNode *tail; unsigned long len; void dup(void *ptr);  // 節點復制函數
        void free(void *ptr); // 節點值釋放函數
        int match(void *ptr , void *key); // 節點值比對函數
}list;
head指針:指向鏈表的頭節點。

tail指針:指向鏈表的最后一個節點。

len屬性:存儲鏈表中節點的個數。

 

6.INTSET編碼方式

 

Redis使用intset結構來表示整數集合。

typedef struct inset { uint32_t encoding; uint32_t length; int8_t contents[]; }intset;
encoding屬性:標識contents數組的類型,支持INTESET_ENC_INT16、INTESET_ENC_INT32、INTESET_ENC_INT64。

length屬性:存儲整數集合中元素的個數。

contents數組:存儲整數集合中的元素(從小到大進行排序,並且保證元素不會重復)

 

Contents升級

當往整數集合中添加一個比當前Contents數組類型還要大的元素時,將要進行Contents的升級。

1.對Contents數組進行擴容( (length + 1) * 新類型的大小)

2.將原有的元素轉換成與新元素相同的類型,然后移動到正確的位置上。

3.將新元素添加到數組當中。

4.將encoding屬性修改為新元素的類型。

*contents數組不支持降級,一旦對contents數組進行了升級那么就要一直保持升級后的狀態。

 

7.HASHTABLE編碼方式

 

8.SKIPLIST編碼方式

 

通過在每個節點中維護多個指向其他節點的指針,從而達到快速訪問的目的。

Redis使用zskiplistNode結構來表示跳躍表中的節點.

typedef struct zskiplistNode { struct zskiplistLevel { struct zskiplistNode *forward; unsigned int span; }level[]; struct zskiplistNode *backward; double score; robj *obj; }zskiplistNode 
level[]數組:用於存儲zskiplistLevel,每個zskiplistLevel都包含forward和span屬性,其中forward屬性用於指向表尾方向的其他節點,而span屬性則記錄了forward指針所指向的節點距離當前節點的跨度(forward指針遵循同層連接的原則)

backward屬性:指向上一個節點的指針。

score屬性:存儲元素的分數。

obj指針:指向元素的地址(字符串對象)

每次創建一個新的跳躍表節點時,會隨機生成一個介於1到32之間的值作為level數組的大小。

 

Redis使用zskiplist結構來持有zskiplistNode

typedef struct zskiplist { struct zskiplistNode *header,*tail; unsigned long length; int level; }zskiplist;
header指針:指向跳躍表的頭節點。

tail指針:指向跳躍表的最后一個節點。

length屬性:存儲跳躍表中節點的個數(不包括表頭節點)

level屬性:跳躍表中節點level的最大值(不包括表頭節點)

*跳躍表中存在表頭節點,表頭節點一共有32個level,即數組的大小為32。

 

遍歷zskiplist的流程

1.通過zskiplist的header指針訪問跳躍表中的頭節點。

2.從下一個節點最高的level開始往下遍歷,若下一個節點的最高level比當前節點的最高level要大,則從當前節點的最高level開始往下遍歷。

3.當不存在下一個節點時,遍歷結束。

 

 

5.Redis中的對象

 

Redis中一共包含五種對象,分別是字符串對象、列表對象、哈希對象、集合對象、有序集合對象,每種對象都支持多種編碼方式,不同的編碼方式之間使用不同的數據結構進行存儲。

 

Redis各個對象支持的編碼方式

 

1.字符串對象

字符串對象支持INT、EMBSTR、ROW三種編碼方式。

 

INT編碼方式

如果字符串的值是整數,同時可以使用long來進行表示,那么Redis將會使用INT編碼方式。

 

INT編碼方式會將RedisObject中的*ptr指針直接改寫成long ptr,ptr屬性直接存儲字面量。

 

EMBSTR編碼方式

如果字符串的值是字符,同時字符串的大小小於32個字節,那么Redis將會使用EMBSTR編碼方式。

 

ROW編碼方式

如果字符串的值是字符,同時字符串的大小大於32個字節,那么Redis將會使用ROW編碼方式。

*EMBSTR和ROW編碼方式在內存中都會創建字符串對象(SDS),區別在於EMBSTR編碼方式中RedisObject和SDS共同使用同一塊內存單元,Redis內存分配器只需要分配一次內存,而ROW編碼方式需要單獨的為RedisObject和SDS分配內存單元。


編碼轉換

如果字符串的值不再是整數或者用long無法進行表示,那么INT編碼方式將會轉換成ROW編碼方式。

如果字符串的值其大小大於32個字節,那么EMBSTR編碼方式將會轉換成ROW編碼方式。

*INT編碼方式不能轉換成EMBSTR編碼方式。

 

字符串共享對象

Redis在啟動時會初始化值為0~9999的字符串對象作為共享對象,當set一個Key其Value是在0~9999范圍時,會直接使用該共享對象,DictEntry中的value指針直接指向該共享的字符串對象。

在集群模式中,Redis的每個節點啟動時都會初始化值為0~9999的字符串對象作為共享對象。

在RedisV4.0以上,使用Object refcount命令不再返回共享對象實際被引用的次數,而是直接返回Integer.MAX_VALUE。

  

2.列表對象

列表對象支持ZIPLIST、LINKEDLIST兩種編碼方式。

 

ZIPLIST編碼方式

如果列表對象保存的元素的數量少於512個,同時每個元素的大小都小於64個字節,那么Redis將會使用ZIPLIST編碼方式。 

  

LINKEDLIST編碼方式

如果列表對象保存的元素的數量多於512個,或者元素的大小大於64個字節,那么Redis將會使用LINKEDLIST編碼方式。

 

編碼轉換

如果列表對象保存的元素的數量多於512個,或者元素的大小大於64個字節,那么Redis將會使用LINKEDLIST編碼方式。

可以通過list-max-ziplist-entries和list-max-ziplist-value參數,調整列表對象ZIPLIST編碼方式中最多可以保存元素的個數以及每個元素的最大大小。 

 

3.哈希對象

哈希對象支持ZIPLIST和HASHTABLE兩種編碼方式。

 

ZIPLIST編碼方式

如果哈希對象保存的鍵值對的數量少於512個,同時每個鍵值對中的鍵和值的大小都小於64個字節,那么Redis將會使用ZIPLIST編碼方式。

 

HASHTABLE編碼方式

如果哈希對象保存的鍵值對的數量多於512個,或者鍵值對中的鍵或值的大小大於64個字節,那么Redis將會使用HASHTABLE編碼方式。

 

編碼轉換

如果哈希對象保存的鍵值對的數量多於512個,或者鍵或值中的鍵和值的字符串的大小大於64個字節,那么Redis將會使用HASHTABLE編碼方式。

可以通過hash-max-ziplist-entries和hash-max-ziplist-value參數,調整哈希對象ZIPLIST編碼方式中最多可以保存元素的個數以及每個鍵值對中的鍵和值的字符串的最大大小。

 

4.集合對象

集合對象支持INTSET和HASHTABLE兩種編碼方式。

 

INTSET編碼方式

如果集合對象保存的元素的數量少於512個,同時每個元素都是整數,那么Redis將會使用INTSET編碼方式。

 

HASHTABLE編碼方式

如果集合對象保存的元素的數量多於512個,或者元素不是整數,那么Redis將會使用HASHTABLE編碼方式。

 

編碼轉換

如果集合對象保存的元素的數量多於512個,或者元素不是整數,那么Redis將會使用HASHTABLE編碼方式。

可以通過set-max-intset-entries參數,調整集合對象INTSET編碼方式中最多可以保存元素的個數。

 

5.有序集合對象

有序集合對象支持ZIPLIST和SKIPLIST兩種編碼方式。

 

ZIPLIST編碼方式

如果有序集合對象保存的元素的數量少於128個,同時每個元素的大小都小於64個字節,那么Redis將會使用ZIPLIST編碼方式。

 

SKIPLIST編碼方式

如果有序集合對象保存的元素的數量多於128個,或者元素的大小大於64個字節,那么Redis將會使用SKIPLIST編碼方式。

 

編碼轉換

如果有序集合對象保存的元素的數量多於128個,或者元素的大小大於64個字節,那么Redis將會使用SKIPLIST編碼方式。

可以通過zset-max-ziplist-entries和zset-max-ziplist-value參數,調整有序集合對象ZIPLIST編碼方式中最多可以保存元素的個數以及每個元素的最大大小。

 

 

6.Redis內存分配器

 

Redis提供了jemalloc、libc、tcmalloc內存分配器,默認使用jemalloc,需要在編譯時指定。

 

Jemalloc內存分配器

jemalloc內存分配器將內存划分為小、大、巨大三個范圍,每個范圍又包含多個大小不同的內存單元。

*DictEntry、RedisObject以及對象在初始化時,Redis內存分配器都會分配一個合適的內存大小。

*如果頻繁修改value,且value的值相差很大,那么有可能導致編碼轉換或者已分配的內存空間不足,那么redis內存分配器需要重新為對象分配內存,然后釋放掉對象之前所占用的內存。

 

 

7.Redis內存監控

 

可以使用info memory命令查看Redis內存的使用情況

used_memory:redis有效數據占用的內存大小(包括使用的虛擬內存)

uesd_memory_rss:redis有效數據占用的內存大小(不包括使用的虛擬內存)、redis進程所占用的內存大小、內存碎片(與TOP命令查看的內存一直)

mem_fragmentation_ratio(內存碎片率) = used_memory_rss / used_memory ,由於一般不會使用虛擬內存,同時redis進程所占用的內存相對使用的內存來說很少,因此這個比例可以認為是內存碎片率。

mem_allocator:redis內存分配器,可選jemalloc(默認)、libc、tcmalloc

*max_memory配置的是Redis有效數據最大可以使用的內存,由於存在內存碎片,因此Redis實際占用的內存大小最終一定會比max_memory要大。

 

關於內存碎片率

mem_fragmentation_ratio = used_memory_rss / used_memory ;

當內存碎片率 < 1時,表示redis正在使用虛擬內存,當內存碎片率嚴重 > 1,表示redis存在大量的內存碎片。

*內存碎片率在1~1.1之間是比較健康的狀態。

 

產生內存碎片的原因

1.如果頻繁修改value,且value的值相差很大,那么有可能導致編碼轉換或者已分配的內存大小不足,那么redis內存分配器需要重新為對象分配內存,然后釋放掉對象之前所占用的內存,如果釋放掉的內存無法被操作系統所回收,那么就形成了內存碎片。

2.redis的內存淘汰機制,根據內存淘汰策略刪除一部分的Key,但釋放的內存無法被操作系統所回收。

*根本原因是redis釋放的內存無法被操作系統所回收。

 

解決內存碎片的方法

1.重啟Redis服務,會自動讀取RDB文件進行數據的恢復,重新為對象分配內存。

2.Redis4.0提供了清除內存碎片的功能

#自動清除
activedefrag yes

#手動執行命令清除
memory purge

 

 

8.Redis監視器

 

客戶端向服務器發送命令請求時,服務器除了會執行相應的命令以外,還會將關於這條命令的相關信息轉發給各個監視器。

客戶端可以通過執行monitor命令讓自己變成一個監視器,實時接收服務器當前正在執行的命令的相關信息。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM