Redis用到的底層數據結構有:簡單動態字符串、雙端鏈表、字典、壓縮列表、整數集合、跳躍表等,Redis並無直接使用這些數據結構來實現鍵值對數據庫,而是基於這些數據結構建立了一個對象系統,這個系統包括字符串對象、列表對象、哈希對象、集合對象和有序結合對象共5種類型的對象。
好比執行以下命令時:
Redis將在數據庫中建立一個新的鍵值對,其中鍵是一個字符串,一個保存着"name"的sds;值是一個字符串,一個保存着"intrack"的sds。redis
Redis使用簡單字符串(sds)做爲字符串顯示,有如下優點:
- 常數複雜度獲取字符長度
- 避免緩衝區溢出
- 減小修改字符操做時引發的內存分配次數(注意:free內存大小最大爲1M)
- 二進制安全的
- 兼容部分C字符串函數(由於字符串後面以'\0'結尾)
2 鏈表
鏈表在Redis應用很普遍,好比列表鍵底層實現之一就是鏈表,當一個列表鍵包含了數量比較多的元素,或者列表中包含元素是比較長的字符串時,redis就使用鏈表做爲其底層實現。除了列表鍵以外,發佈與訂閱、慢查詢、監視器等功能也用到了鏈表,Redis服務器自己還使用了鏈表來保存多個客戶端的狀態信息,以及使用鏈表來構建客戶端輸出緩衝區。
typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
} listNode;

typedef struct list {
listNode *head;
listNode *tail;
void *(*dup)(void *ptr);
void (*free)(void *ptr);
int (*match)(void *ptr, void *key);
unsigned long len;
} list;
list結構鏈表提供了表頭指針head、表尾指針tail及鏈表長度len,而dup/free/match用於實現存儲類型無關鏈表所需的類型特性函數。dup用於複製一個鏈表節點、free用於釋放一個鏈表節點、match用於匹配鏈表節點和輸入的值是否相等。算法
- 鏈表被普遍用於實現Redis的各類功能,好比列表鍵、發佈與訂閱、慢查詢、監視器等。
- 每一個鏈表節點由一個listNode結構表示,每一個節點都有一個指向前置節點和後置節點的指針,因此Redis中鏈表是雙向鏈表。
- 每一個鏈表使用一個list結構表示,這個結構有表頭節點指針、表尾節點指針、以及鏈表長度信息。
- 鏈表表頭節點的前置節點和表尾的後置節點都指向NULL,因此Redis鏈表是無環鏈表。
- 經過將鏈表設置不一樣類型的特定函數,使得Redis鏈表可存儲不一樣類型的值。
3 字典
字典,又稱爲符號表、映射,是一種保存鍵值對的數據結構。字典在Redis中應用至關普遍,好比Redis的數據庫就是在使用字典做爲底層實現的,對於數據庫的CURD操做就是構建在對字典的操做上的。
好比當執行如下命令時:
redis> set msg "hello world"
在數據庫中建立了一個鍵爲msg,值爲hello world的鍵值對時,這個鍵值對就保存在表明數據庫的字典裏面的。除了用做數據庫以外,字典仍是哈希鍵的底層實現之一。數據庫
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask; // 哈希表大小掩碼,用於計算索引值
unsigned long used; // 已有節點數量
} dictht;

typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
key保存鍵值對中的鍵,v屬性保存值信息,值能夠是一個指針/uint64_t整數/int64_t整數。next指向下一個哈希表節點指針,解決鍵值對衝突問題。數組

Redis的字典由dict結構定義:
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
int iterators; /* number of iterators currently running */
} dict;
type屬性和privdata屬性是針對不一樣類型的鍵值對,爲建立能夠存儲多種類型的字典而設置的。緩存
type屬性是一個指向dictType結構的指針,每一個dictType結構保存了一組用於操做特定類型鍵值對的函數,Redis會爲不一樣用途的字典設置不一樣的特定函數。privdata屬性則保存了須要傳給那些特定函數的可選參數。
ht屬性包含2項,每一項都是一個dictht哈希表,通常狀況下字典只使用ht[0],ht[1]只在對ht[0]哈希表進行rehash時使用。
typedef struct dictType {
unsigned int (*hashFunction)(const void *key); // 哈希計算
void *(*keyDup)(void *privdata, const void *key); // 複製鍵的函數
void *(*valDup)(void *privdata, const void *obj); // 複製值的函數
int (*keyCompare)(void *privdata, const void *key1, const void *key2); // 比較鍵的函數
void (*keyDestructor)(void *privdata, void *key); // 銷燬鍵的函數
void (*valDestructor)(void *privdata, void *obj); // 銷燬值的函數
} dictType;

- 字典被普遍用於實現Redis的各類功能,其中包括數據庫和哈希鍵。
- Redis中字典使用哈希表做爲底層實現,每一個字典有2個哈希表,一個平時使用,另外一個只在rehash時使用。
- 當字典做爲數據庫的底層實現,或者做爲哈希鍵的底層實現時,使用MurmurHash2算法計算鍵的哈希值。
- 哈希表使用分離鏈接法解決鍵衝突問題,被分配到同一個索引上多個鍵值會鏈接成一個單向鏈表。
- 在對哈希表進行擴展或者縮容操做時,須要將現有哈希表中鍵值對rehash到新哈希表中,這個rehash過程不是一次性完成的,而是漸進的。
4 跳躍表
跳躍表是一種有序數據結構,它經過在每一個節點維持多個指向其餘節點的指針來達到快速訪問節點的目的。Redis使用跳躍表做爲有序集合的底層實現之一,若是一個有序集合包含的元素數量較多,或者有序集合元素是比較長的字符串,Redis就會使用跳躍表做爲有序集合的底層實現。
Redis中的跳躍表由zskiplistNode和zskiplist兩個結構體定義,其中zskiplistNode表示跳躍表節點,zskiplist表示跳躍表信息。
typedef struct zskiplistNode {
robj *obj; // Redis對象
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
} level[];
} zskiplistNode;
span屬性用於記錄兩個節點之間的距離,指向NULL的forward值都爲0。節點的分值score是一個浮點數,跳躍表中全部節點都按照分值從小到大排列。obj屬性必須指向一個字符串對象,而字符串則保存着一個sds。安全
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
header和tail指針分表指向跳躍表的表頭和表尾節點,經過length屬性記錄表長度,level屬性用於保存跳躍表中層高最大的節點的層高值。每一個跳躍表節點層高都是1~32的隨機值,在同一個跳躍表中,多個節點能夠包含相同的分值,可是每一個節點的成員對象必須是惟一的。當分值相同時,節點按照成員對象的大小排序。服務器
5 整數結合
整數集合是集合鍵的底層實現之一,當一個集合只包含整數元素時,而且每一個集合的元素數量很少時,Redis就會使用整數集合做爲集合建的底層實現。
整數集合是Redis中用於保存整數的集合抽象數據結構,它能夠保存int16_t/int32_t/int64_t的值,而且保證集合中元素不會重複。
typedef struct intset {
uint32_t encoding; // 16/32/64編碼
uint32_t length; // 數組長度
int8_t contents[];
} intset;
contents數組用於存儲整數,數組中的值按照值的大小從小到大有序排列,而且不會包含重複項。當encoding編碼的是int型整數的話,那麼contents數組中每4項用於保存一個int型整數。數據結構
由於contents數組能夠保存int16/int32/int64的值,因此可能會出現升級現象,也就是原本是int16編碼方式,須要升級到int32編碼方式,這時數組會擴容,而後將新元素添加到數組中,這期間數組始終會保持有序性。一旦整數集合進行了升級操做,編碼就會一直保持升級後的狀態,也就是不會出現降級操做。
6 壓縮列表
壓縮列表是列表鍵和哈希表鍵的底層實現之一,當一個列表鍵只包含少許列表項,而且每一個列表項是小整數或者短的字符串,那麼會使用壓縮列表做爲列表鍵的底層實現。
壓縮列表是Redis爲了節約內存開發的,由一系列特殊編碼的連續內存塊組成的順序性數據結構。一個壓縮列表能夠包含多個節點,每一個節點保存一個字節數組或者一個整數值。
壓縮列表按照固定格式來存儲的,相似於存儲多個TLV消息同樣。
7 Redis中的對象
Redis中共有5種不一樣類型的對象,分別是字符串、列表、哈希表、集合、有序集合。這些對象都是基於以上分析的數據結構來構建的,而且每種對象都用到了至少一種以上。Redis對象還實現了引用計數技術的內存回收技術,當再也不使用某個對象時,能夠及時釋放其內存;經過了引用計數實現了對象共享機制,節約內存;Redis的對象帶有訪問時間記錄信息,該信息可用於計算該對象空轉時間,在啓動了maxmemroy功能下,空轉時間較長的鍵優先被刪除。
Redis中使用對象表示鍵和值,當新建一個鍵值對時,Redis至少建立2個對象,一個是鍵對象,另外一個是值對象。
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
int refcount;
void *ptr;
} robj;
type表示對象類型,對於Redis鍵值對來講,鍵永遠都是字符串,值能夠是字符串、列表、哈希表、集合、有序集合中的一種。encoding表示對象編碼,也就是該對象使用什麼底層數據結構實現。ptr指向對象的底層數據結構。app

7.1 字符串對象函數
字符串對象能夠是int、raw或者embstr。若是一個字符串時整數,而且可用long型表示,那麼該字符串對象編碼就是int。若是字符串長度大於39字節,那麼將使用一個簡單動態字符串(sds)保存,並將對象編碼設置爲raw。若是字符串長度小於等於39字節,則字符串以編碼方式embstr來保存該字符串值。

embstr編碼方式是redisObject結構和sdshdr結構在一塊內存中,使用embstr對象只須要調用一次內存分配函數便可,而raw方式須要調用2次。由於在同一塊內存中,因此對緩存是友好的。
注意,字符串的編碼方式是能夠轉換的,好比set num 1後執行append num hello,則會致使編碼方式由int到raw轉換。
7.2 列表對象
列表對象的編碼能夠是ziplist或者linkedlist。ziplist使用功能壓縮列表做爲底層實現,每一個壓縮列表節點保存一個列表元素。
執行如下命令時,Redis會建立一個列表存儲nums的值:
若是列表不是使用的ziplist實現,而是linkedlist實現,則底層實現以下所示:
注意,linkedlist編碼的列表對象在底層雙端列表中包含了多個字符串對象,這個嵌套字符串對象行爲在哈希表、集合中都會出現,字符串對象是Redis五種類型中惟一一種會被其餘四種類型對象嵌套的對象。
列表既然有ziplist和linkedlist兩種底層實現,那麼列表到底使用哪種呢?
列表對象保存的全部字符串長度都小於64字節而且列表保存的元素數量小於512個時使用ziplist編碼實現,不然使用linkedlist編碼實現。注意這個512的值是能夠修改的,具體參見配置項list-max-ziplist-value和list-max-ziplist-entries選項。
7.3 哈希對象
哈希對象的編碼能夠是ziplist和hashtable。ziplist編碼的哈希對象使用壓縮列表做爲底層實現,當有新的鍵值對要加入哈希對象時,會先將保存了鍵的壓縮列表節點推入到壓縮列表表尾,再將保存了值的壓縮列表節點推入到列表表尾。這樣的話,一對鍵值對老是相鄰的,而且鍵節點在前值節點在後。
若是man編碼爲ziplist方式,則其對象所使用的壓縮列表以下:
若是hashtable編碼的哈希對象使用字典做爲底層實現,則哈希對象中的每一個鍵值對都是字典鍵值對來保存,此時哈希對象以下:
哈希對象既然有ziplist和hashtable兩種底層實現,那麼其到底使用哪種呢?
列哈希象保存的全部字符串長度都小於64字節而且列表保存的元素數量小於512個時使用ziplist編碼實現,不然使用hashtable編碼實現。注意這個512的值是能夠修改的,具體參見配置項hash-max-ziplist-value和hash-max-ziplist-entries選項。
7.4 集合對象
集合對象的編碼能夠是intset和hashtable。intset編碼的集合對象使用整數集合做爲底層實現,全部元素都保存在整數集合中。另外一方面,使用hashtable的集合對象使用字典做爲底層實現,字典中每一個鍵都是一個字符串對象,即一個集合元素,而字典的值都是NULL的。
既然集合有intset和hashtable兩種底層實現,那麼其到底使用哪種呢?
集合對象全部的元素都是整數值而且集合對象數量不超過512個時使用intset實現,不然使用hashtable實現。注意,這裏的512值是能夠修改的,具體參見配置項set-max-intset-entries選項。
7.5 有序集合對象
有序集合對象的編碼能夠是ziplist和skiplist。ziplist編碼的壓縮列表對象使用壓縮列表做爲底層實現,每一個集合元素使用兩個緊挨着的壓縮列表節點保存,第一個保存集合元素,第二個保存集合元素對應的分值。壓縮列表內集合元素按照分值大小進行排序,分值較小的在前,分值大的在後。
以上命令對應的壓縮列表視圖以下所示:
skiplist編碼的有序集合對象使用zset結構做爲底層實現,一個zset結構同時包含一個字典和一個跳躍表。
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
zset中的zsl跳躍表按分值從小到大保存了全部集合元素,每一個跳躍表節點保存一個集合元素,跳躍表節點的object屬性保存元素的成員,score屬性保存元素的分值。經過該跳躍表,能夠對有序集合進行範圍型操做,好比zrank、zrange命令就是基於跳躍表實現的。
zset中的dict字典爲有序集合建立了一個從成員到分值的映射,字典中的每一個鍵值對都保存了一個集合元素,字典的鍵保存集合元素的成員,字典的值保存集合成員的分值。經過該字典,能夠O(1)複雜度查找到特定成員的分值,zscore命令就是根據這一特性來實現的。經過字典+skiplist做爲底層實現,各取所長爲我所用。
8 對象的其餘特性
對象空轉時長
redisObject結構中有一項(unsigned lru;)是記錄對象最後一次訪問的時間,使用命令object idletime key能夠顯示對象空轉時長。
當Redis打開maxmemory選項時,而且Redis用於回收內存的算法爲volatile-lru或者allkey-lru時,那麼當Redis佔用內存超過了maxmemory選項設定的值時,空轉時長較高的那部分鍵會優先被Redis釋放,從而回收內存。
內存回收
C不具有內存回收功能,Redis在本身對象機制上實現了引用計數功能,達到內存回收目的,每一個對象的引用計數值在redisObject中的(int refcount;)來記錄。當建立一個對象或者該對象被從新使用時,它的引用計數++;當一個對象再也不被使用時,它的引用計數--;當一個對象的引用計數爲0時,釋放該對象內存資源。
對象共享
對象的應用計數另一個功能就是對象的共享,當一個對象被另一個地方使用時,能夠直接在該對象引用計數上++就行。注意:Redis只對包含整數值的字符串對象進行共享。
參考資料:
一、《Redis設計與實現》