Redis設計原理

1.簡介

 

Redis中的每一個Key-Value在內存中都會被劃分紅DictEntry、RedisObject以及具體對象,其中DictEntry又分別包含指向Key和Value的指針(以RedisObject的形式)以及指向下一個DictEntry的指針。html

 

 

 

 

Key固定是字符串,所以使用字符串對象來進行表示,Value能夠是字符串、列表、哈希、集合、有序集合對象中的任意一種。redis

Redis提供了五種對象,每種對象都須要使用RedisObject進行表示。數據庫

 

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屬性:存儲對象被引用的次數。post

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

 

使用type命令能夠查看對象的類型。優化

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

使用object idletime命令能夠查看對象的空轉時間(即多久沒有被訪問,並不會刷新當前RedisObject的lru屬性)

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

*這些命令都是經過Key找到對應的Value再從Value對應的RedisObject中進行獲取。

 

 

2.字符串

 

Redis沒有直接使用C語言的字符串,而是自定義了一種字符串類型,以對象的形式存在(C語言的字符串只是單純的字面量,不可以進行修改)

Redis使用sdshdr結構來表示字符串對象(SDS)

struct sdshdr { int len; int free; char buf[]; };

len屬性:字符串的長度。

free屬性:未使用的字節數量。

buf數組:字符串的底層實現用於存儲字符。

 

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

 

SDS相比C語言的字符串

C語言中存儲字符串的字節數組其長度老是N+1(最後一個是結束符),所以一旦對字符串進行追加則須要從新分配內存。

爲了不C字符串的這種缺陷,SDS經過未使用的空間解除了字符串長度和底層數組長度之間的關係,在SDS中buf數組的長度不必定就是字符串長度+1,數組裏面還能夠包含未使用的字節。

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

空間預分配:用於優化SDS保存的字符串的增加操做,當須要對SDS保存的字符串進行增加操做時,程序除了會爲SDS分配所必須的空間之外,還會爲SDS分配額外的未使用空間。

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

 

 

3.字典

 

Redis的字典使用散列表做爲底層實現,同時字典也是Redis數據庫和HashTable編碼方式的底層實現。

 

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

typedef struct dictht { dictEntry **table; unsigned long size; unsigned long sizemask; unsigned long used; }dictht;

table屬性:散列表。

size屬性:散列表的大小。

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

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

*Redis的散列表使用鏈地址法的方式解決散列衝突,最終就是指針數組的形式,數組中的每一個元素都是一個指向DictEntry的指針。


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

複製代碼
typedef struct dictEntry { void *key; union{ void *val; uint_tu64; int64_ts64; }v struct dictEntry next*; }dictEntry; 
複製代碼

key屬性:指向Key的指針(即RedisObject)

value屬性:能夠是一個指向Value的指針(即RedisObject)、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); //複製Key的函數
    void *(*keyDup)(void *privatedata, const void *key); //複製Value的函數
    void *(*valDup)(void *privatedata, const void *obj); //對比Key的函數
    int (*keyCompare)(void *privatdata, const void *key1 , const void *key2); //銷燬Key的函數
    void (*keyDestructor)(void *privatedata, void *key); //銷燬Value的函數
    void (*valDestructor)(void *privatedata, void *obj); }dictType;
複製代碼

 

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

 

在字典中進行查找

以客戶端傳遞的Key做爲關鍵字K,經過dict中的dictType的H(K)散列函數計算散列值,使用dictht[0]的sizemask屬性和散列值計算索引,遍歷索引對應的鏈表,判斷是否存在Key相同的DictEntry,若存在則返回該DictEntry,不然返回NULL。

 

在字典中進行添加和更新操做

以客戶端傳遞的Key做爲關鍵字K,經過dict中的dictType的H(K)散列函數計算散列值,使用dictht[0]的sizemask屬性和散列值計算索引,遍歷索引對應的鏈表,判斷是否存在Key相同的DictEntry,若不存在Key相同的DictEntry,則建立表明Key的SDS對象和RedisObject以及表明Value的對象和RedisObject,而後建立一個DictEntry並分別指向Key和Value對應的RedisObject,最終將該DictEntry追加到鏈表的最後一個節點中,若存在Key相同的DictEntry,則判斷當前的命令是否知足Value對應的類型,若知足則進行更新,不然報錯。

*建立和更新操做是相對的,當不存在則建立不然進行更新。

 

在字典中進行刪除操做

以客戶端傳遞的Key做爲關鍵字K,經過dict中的dictType的H(K)散列函數計算散列值,使用dictht[0]的sizemask屬性和散列值計算索引,遍歷索引對應的鏈表,找到Key相同的DictEntry進行刪除。

 

3.2 散列表的擴容和縮容

因爲散列表的負載因子須要維持在一個合理的範圍內,所以當散列表中的元素過多時會進行擴容,過少時會進行縮容。

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

Redis中的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]散列表中的全部dictEntry都會被rehash到ht[1]上,當rehash結束以後將rehashidx屬性的值設爲-1,表示rehash操做已完成。

*在進行漸進式rehash的過程當中,字典會同時使用ht[0]和ht[1]兩個散列表,所以字典的查找、更新、刪除操做會在兩個散列表中進行,若是在ht[0]計算獲得的索引指向NULL則從ht[1]中進行匹配。

 

 

4.Redis提供的編碼方式

 

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

 

4.1 INT編碼方式

INT編碼方式會將RedisObject中的*ptr指針直接改寫成long prt,prt屬性直接存儲整數值。

 

4.2 EMBSTR編碼方式

 

4.3 ROW編碼方式

 

*EMBSTR和ROW編碼方式在內存中都會建立一個RedisObject和SDS,區別在於EMBSTR編碼方式中RedisObject和SDS共同使用同一塊內存單元,Redis內存分配器只須要分配一次內存,而ROW編碼方式中須要分別爲RedisObject和SDS分配內存單元。

 

4.4 ZIPLIST編碼方式

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

zlbytes:記錄了壓縮列表的大小,佔4個字節。

zltail:記錄了壓縮列表表尾節點距離起始位置的大小,佔4個字節。

zllen:記錄了壓縮列表中節點的個數,佔2個字節。

entry:壓縮列表中的節點,大小由節點中保存的內容決定。

zlend:壓縮列表的結束標誌,佔1個字節。

 

若是存在一個指針P指向壓縮列表的起始位置,就能夠根據P+zltail獲得最後一個節點的地址。

 

4.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屬性:存儲鏈表中節點的個數。

 

4.6 INTSET編碼方式

 

 

 

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

typedef struct inset { uint32_t encoding; uint32_t length; int8_t contents[]; }intset;

encoding屬性:contents數組的類型,支持INTESET_ENC_INT1六、INTESET_ENC_INT3二、INTESET_ENC_INT64。

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

contents數組:整數集合的底層實現,集合中的每一個元素在數組中都會按照值從小到大進行排序同時保證元素不會重複。

 

Contents升級

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

 

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

 

2.將數組中現有的元素都轉換成與新元素相同的類型,並將轉換後的元素移動到正確的位置上。

 

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

 

4.修改intset中的encoding屬性爲新的類型。

 

Contents降級

contents數組不支持降級,一旦爲contents數組進行了升級那麼就要一直保持升級後的狀態。

 

4.7 HT編碼方式

 

4.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屬性:指向元素的指針(redisObject->sds)

每次建立一個新的跳躍表節點時,會隨機生成一個介於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訪問跳躍表中的頭節點。

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

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

 

 

5.Redis對象

 

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

 

5.1 字符串對象

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

 

INT編碼方式

若是字符串的值是整數,而且可使用long來進行表示,那麼Redis將會使用INT編碼方式。


INT編碼方式會將RedisObject中的*ptr指針直接改寫成long prt,prt屬性直接存儲整數值。


EMBSTR編碼方式

若是字符串的值是字符,而且其長度小於32個字節,那麼Redis將會使用EMBSTR編碼方式。

ROW編碼方式

若是字符串的值是字符,而且其長度大於32個字節,那麼Redis將會使用ROW編碼方式。

 

*EMBSTR和ROW編碼方式在內存中都會建立一個RedisObject和SDS,區別在於EMBSTR編碼方式中RedisObject和SDS共同使用同一塊內存單元,Redis內存分配器只須要分配一次內存,而ROW編碼方式中須要分別爲RedisObject和SDS分配內存單元。


編碼轉換

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

若是字符串的值其長度大於32個字節,那麼EMBSTR編碼方式將會轉換成ROW編碼方式。

*INT編碼方式和EMBSTR編碼方式在知足條件的狀況下,將會轉換成ROW編碼方式。

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

 

字符串共享對象

Redis在啓動時會初始化值爲0~9999的SDS做爲共享對象,當set一個Key其Value是在0~9999範圍時,會直接使用該共享對象,DictEntry中的Value指針直接指向該共享SDS對應的RedisObject。

在集羣模式中,Redis的每一個節點啓動時都會初始化值爲0~9999的SDS做爲共享對象。

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

 

 

5.2 列表對象

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

 

ZIPLIST編碼方式

若是列表對象保存的全部元素的長度都小於64個字節同時元素的數量小於512個,那麼Redis將會使用ZIPLIST編碼方式。

 

LINKEDLIST編碼方式

若是列表對象保存的元素的長度大於64個字節或元素的數量大於512個,那麼Redis將會使用LINKEDLIST編碼方式。

 

編碼轉換

若是列表對象保存的元素的長度大於64個字節或元素的數量大於512個,那麼Redis將會使用LINKEDLIST編碼方式。

能夠經過list-max-ziplist-value和list-max-ziplist-entries參數調整列表對象ZIPLIST編碼方式所容許保存的元素的最大值以及最多能夠保存元素的數量。

 

 

5.3 哈希對象

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

 

ZIPLIST編碼方式

若是哈希對象保存的全部鍵值對的鍵和值的字符串長度都小於64個字節同時鍵值對的數量小於512個,那麼Redis將會使用ZIPLIST編碼方式。

 

 

HT編碼方式

若是哈希對象保存的鍵值對的鍵或值的字符串長度大於64個字節或鍵值對的數量大於512個,那麼Redis將會使用HASHTABLE編碼方式。

 

編碼轉換

若是哈希對象保存的鍵值對的鍵或值的字符串長度大於64個字節或鍵值對的數量大於512個,那麼Redis將會使用HASHTABLE編碼方式。

能夠經過hash-max-ziplist-value和hash-max-ziplist-entries參數調整哈希對象ZIPLIST編碼方式所容許保存的元素的最大值以及最多能夠保存元素的數量。

 

 

5.4 集合對象

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

 

INTSET編碼方式

若是集合對象保存的全部元素都是整數同時元素的數量不超過512個,那麼Redis將會使用INTSET編碼方式。

 

HT編碼方式

若是集合對象保存的元素並非整數或元素的數量超過512個,那麼Redis將會使用HASHTABLE編碼方式。

 

編碼轉換

若是集合對象保存的元素並非整數或元素的數量超過512個,那麼Redis將會使用HASHTABLE編碼方式。

能夠經過set-max-intset-entries參數調整集合對象INTSET編碼方式最多能夠保存元素的數量。

 

 

5.5 有序集合對象

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

 

ZIPLIST編碼方式

若是有序集合對象保存的全部元素的字符串長度都小於64個字節同時元素的數量不超過128個,那麼Redis將會使用ZIPLIST編碼方式。

 

SKIPLIST編碼方式

若是有序集合對象保存的元素的字符串長度大於64個字節或元素的數量超過128個,那麼Redis將會使用SKIPLIST編碼方式。

 

編碼轉換

若是有序集合對象保存的元素的字符串長度大於64個字節或元素的數量超過128個,那麼Redis將會使用SKIPLIST編碼方式。

能夠經過zset-max-ziplist-value和zset-max-ziplist-entries參數調整有序集合對象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

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

*max_memory配置的是Redis有效數據最大可以使用的內存大小,不包括內存碎片,所以Redis實際佔用的內存大小最終必定會比max_memory要大。

 

內存碎片率

1.當內存碎片率 < 1時,表示redis正在使用虛擬內存。

2.當內存碎片率嚴重 > 1,表示redis存在大量的內存碎片。

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

有可能產生內存碎片的操做:頻繁更新Value且Value的值相差很大(從新爲對象分配內存,釋放以前的內存)、Redis的內存淘汰機制。

產生內存碎片的根本緣由:Redis釋放的內存沒法被操做系統所回收。

 

解決內存碎片的方法

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

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

#運行期自動清除
activedefrag yes

#手動執行命令清除
memory purge

 

 

8.Redis監視器

 

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

經過執行monitor命令,客戶端能夠將本身變成一個監視器,實時接收服務器當前正在執行的命令請求的相關信息。

 

« 上一篇: Nginx反向代理
posted @ 2019-09-05 11:13  辣雞小籃子 閱讀( 99) 評論( 0) 編輯 收藏
相關文章
相關標籤/搜索