Redis 基礎數據結構與對象


Redis用到的底層數據結構有:簡單動態字符串、雙端鏈表、字典、壓縮列表、整數集合、跳躍表等,Redis並沒有直接使用這些數據結構來實現鍵值對數據庫,而是基於這些數據結構創建了一個對象系統,這個系統包括字符串對象、列表對象、哈希對象、集合對象和有序結合對象共5種類型的對象。
 

1 簡單動態字符串

redis自定義了簡單動態字符串數據結構(sds),並將其作為默認字符串表示。
struct sdshdr {
unsigned int len;
unsigned int free;
char buf[];
};

 

比如執行如下命令時:
redis> set name intrack

Redis將在數據庫中創建一個新的鍵值對,其中鍵是一個字符串,一個保存着"name"的sds;值是一個字符串,一個保存着"intrack"的sds。

 
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指向對象的底層數據結構。

 

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只對包含整數值的字符串對象進行共享。
 
參考資料:
1、《Redis設計與實現》


免責聲明!

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



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