Redis存儲結構

Redis現在是比較流行的緩存數據庫,一般剛接觸的時候都會發現其可以存儲字符串(string)、哈希表(hash)、列表(list)、集合(set)、有序集合(sorted set)等redis是一個key-value存儲,value可以包含上面列出的多種結構,但是key都是字符串。也就是說key是string類型,value爲上面類型的一種

由於以上每種數據結構的存儲指令在redis中都不一樣,單個看來想要使用redis必須要先區分要存儲的對象的結構,然後選擇相應的指令。但是這樣使用起來確實是很不利的,如果一次要存入多種形式的值,我就要實現多種存儲方式。

爲了便於開發和使用redis引入了對象,即對象存儲。上面的每種數據結構都是一種對象,所以,在項目中只需要實現對象的存儲即可。

Redis中每個對象都有一個redisObject結構,該結構中和保存數據相關的三種屬性分別是存儲數據的類型type、值的編碼屬性encoding和指針ptr屬性:

typedef struct redisObject{
//類型
unsigned type:4;

//編碼
unsigned encoding:4;

//指向底層實現數據結構的指針
void *ptr

//虛擬內存和其他信息等.....
}robj;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
類型常量對象的名稱type值
REDIS_STRING字符串對象string
REDIS_LIST列表對象list
REDIS_HASH哈希對象hash
REDIS_SET集合對象set
REDIS_ZSET有序集合對象zset

獲取存儲值的類型編碼指令: 
TYPE key

如,我在redis中存入一個字符串的值:

[root@iZ8vb8r420ejxfron03cj7Z ~]# redis-cli
127.0.0.1:6379> set msg "rhett"  
OK
127.0.0.1:6379> get msg
"rhett"
127.0.0.1:6379> type msg
string
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
編碼常量對象的名稱type值
REDIS_ENCODING_INT整數int
REDIS_ENCODING_EMBSTRembstr編碼的簡單動態字符串(SDS)list
REDIS_ENCODING_RAW簡單動態字符串raw
REDIS_ENCODING_HT字典hashtable
REDIS_ENCODING_LINKEDLIST雙端鏈表linkedlist
REDIS_ENCODING_ZIPLIST壓縮列表ziplist
REDIS_ENCODING_INTSET整數集合intset
REDIS_ENCODING_SKIPLIST跳躍表和字典skiplist

redis中的示例:

[root@iZ8vb8r420ejxfron03cj7Z ~]# redis-cli
127.0.0.1:6379> set msg "rhett"  
OK
127.0.0.1:6379> object encoding msg
"embstr"
  • 1
  • 2
  • 3
  • 4
  • 5

特點:快速、非關係、內存存儲、不使用表(必要時才用,少量數據,專屬命令)

比較:MySQL:關係數據庫,大量,增改刪查

 

Redis:服務器關閉時,緊湊的格式將存儲在內存中的數據寫入硬盤

 

持久化方法:1.時間點轉儲:指定時間段內有指定數量的寫操作執行

                            2.將所有修改了數據庫的命令全部寫入一個只追加文件裏面

 

結構類型(5種)

String:字符串、整數、浮點數

List:鏈表

Set:唯一無序集,不重複

Hash:鍵值對,無序散列表

Zset:有序集

 

String命令:對給定鍵中的值獲取GET、設置SET、刪除DEL

$redis-cli

redis 127.0.0.1:6379>set hello world

redis 127.0.0.1:6379>get hello

redis 127.0.0.1:6379>del hello

 

List:

RPUSH:推入右端,返回長度

LRANGE:獲取列表在給定範圍上的所有值

LINDEX:獲取列表在給定位置上的單個元素

LPOP:從左端彈出一個值並返回彈出的值

 

Set:

SADD:將給定元素加入集合

SMEMBERS:返回集合包含的所有元素

SISMEMBER:檢查給定元素是否存在於集合中

SREM:如果在,移除

 

Hash:

HSET:關聯指定鍵值對

HGET:獲取鍵的值

HGETALL:獲取所有鍵值對

HDEL:移除鍵

 

Zset:

ZADD:給定分值的成員添加到有序集合裏

ZRANGE:根據元素位置,從中取多個元素

ZRANGEBYSCORE:獲取給的分值範圍內所有元素

ZREM:刪除給定成員

下面以redis3.2的正式版源碼分析集中存儲機構:

在Redis內部,有非常多的數據結構:sds(簡單動態字符串),list,intset(整數集合),hash(字典),zskiplist(跳躍表),ziplist(壓縮表)等。
1. sds
typedef char *sds;
sds是一種簡單動態字符串,而sdshdr封裝了C原生字符串,並在其基礎上,增加了一些功能,使之後對它的調用簡單易懂可擴展。
/*下面是sds數據結構的具體實現*/
 
/* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
這是sdshdr的具體實現結構體,它作爲sds的持有者,對sds進行存儲和處理,len表示sds的長度,alloc表示分配了的長度,這樣方便擴展;flags標誌來判斷使用哪個類型;buf[]則作爲sds的真正儲存數組,關係大致如下:


Redis採用動態字符串的形式,用len記錄長度,這樣可以在O(1)的複雜度內獲取字符串長度;根據不同的類型和字符串的長短,分別用不同類型的sdshdr,可以節約不少空間;將alloc和len分離,可以在一定的範圍內節省分配內存所用的時間;在Redis中,運用了大量的指針移動技巧來獲取void*對象,也提高了程序的運行效率。例如:
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
通過s-sizeof(sdshdr)來獲取sdshdr的地址;再如:
static inline size_t sdslen(const sds s) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
return SDS_TYPE_5_LEN(flags);
case SDS_TYPE_8:
return SDS_HDR(8,s)->len;
case SDS_TYPE_16:
return SDS_HDR(16,s)->len;
case SDS_TYPE_32:
return SDS_HDR(32,s)->len;
case SDS_TYPE_64:
return SDS_HDR(64,s)->len;
}
return 0;
}
運用s[-1]來獲取flags,判斷類型。
其實,運用指針的移動而不是數組的下標,還保證了字符串的二進制安全,在遇到'\0'的情況下不會中斷。
sds字符串在Redis裏的應用:
    1.保存數據庫中字符串的值
    2.用作緩衝區:
        AOF模塊的緩衝區,
        客戶端狀態中的輸入緩衝區。

2.list
看看list的實現,非常有助於複習一遍數據結構中鏈表的實現原理,建議大家仔細閱讀源碼。
typedef struct listNode { /*節點*/
struct listNode *prev;
struct listNode *next;
void *value; /*value用函數指針類型,決定了value可以是sds,list,set,dict等類型*/
} listNode;
以上是listNode的數據結構。
typedef struct list { /*鏈表結構*/
listNode *head; /*頭節點*/
listNode *tail; /*尾節點*/
/*類似java類裏的的方法,方便調用*/
void *(*dup)(void *ptr); /*複製節點*/ //說實話,我不是很懂這個函數指針的意思,如有清楚地可以給我留言,謝謝。
void (*free)(void *ptr); /*釋放節點*/
int (*match)(void *ptr, void *key); /*匹配節點,返回key值得index,但是我不清楚他在那裏實現的*/
unsigned long len; /*記錄鏈表的長度*/
} list;
Redis中,list的實現是一個雙端鏈表,這樣可以方便的獲取其前後的節點值,方便之後對節點的查找;Redis通過list來對listNode進行持有,分別記錄list的頭尾節點和list長度,可在O(1)的時間複雜度上進行查找;


另外,list還實現了迭代器對鏈表進行遍歷,可正向可反向,非常方便,代碼如下;
typedef struct listIter {
listNode *next;
int direction; //標註迭代器的運行方向
} listIter;
list在Redis中運用相當廣泛,除了實現列表外,發佈和訂閱、慢查詢、監視器等功能也使用了鏈表來獲取,另外,Redis服務器還使用鏈表來持有 多個客戶端的狀態信息,以及用鏈表來構建客戶端輸出緩衝區。
注:Redis中void* 來修飾value值,所以value可以是任意類型的數據,void*在這裏實現了一種類似多態的思想。
3.dict(字典)
字典結構是整個Redis的核心數據結構,基本上是其內部結構的縮影。
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
dictEntry是最核心的字典結構的節點結構,它保存了key和value的內容;另外,next指針是爲了解決hash衝突,字典結構的hash衝突解決方法是拉鍊法,對於hashcode重複的節點以鏈表的形式存儲。
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask; /*hash表的掩碼,總是size-1,用於計算hash表的索引值*/
unsigned long used;
} dictht;
dictht是節點dictEntry的持有者,將dictEntry結構串起來,table就是hash表,其實dictEntry *table[]這樣的書寫方式更容易理解些,size就是table數組的長度,used標誌已有節點的數目。
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;
dict是最外層的字典結構的接口形式,type標誌類型,privdata標誌其私有數據,dict持有兩個dictht結構,一個用來存儲數據,一個用來在rehash時使用,rehashidx標誌是否正在rehash(因爲Redis中rehash是一個漸近的過程,正在rehash的時候rehashidx記錄rehash的階段,否則爲-1)。
注:rehash是一個爲了讓負載因子(load_factor=used/size)控制在一個合理的範圍內而重新分配內存和擴展結構的過程。
iterators是一個迭代器,用於記錄當前迭代的數目。

上面的圖來自網絡,非常清晰的記錄了dict內部結構之間的關係。
注:因爲dictEntry節點組成的鏈表沒有子項鍊表尾部的指針,所以新加的節點一般都加在鏈表的頭部,排在已有節點的前面,因爲這樣的時間複雜度爲O(1)。
字典結構在Redis中廣泛應用,包括數據庫和hash鍵值對。
4.intset
typedef struct intset { /*整數集合的數據結構*/
uint32_t encoding; //編碼方式
uint32_t length;
int8_t contents[];
} intset;
當一個集合元素只有整數並且數量元素不多的時候,可以選擇用整數集合來作爲其底層實現。整數集合的數據結構如上所示。
重點說一下這個contents數組,它存儲集合中的內容,並且以從小到大的順序排列,並保證其沒有重複的元素。
雖然定義中其類型爲int8_t,但具體編碼方式還是取決於encoding。
當最大的數在以上取值範圍之內是便會升級到這個更大範圍的數據類型,但是如果移除了這個最大取值,不會降級。
分範圍定義其類型有兩個好處:提高其靈活性,節約內存。但是也增加了升級的開銷。
在Redis 中,整數集合的應用範圍不是很廣,只在實現集合時用到。
5. zskiplist(跳躍表)
對於不瞭解跳躍表的可以去這個地方看看,瞭解一下:http://blog.nosqlfan.com/html/3041.html。
跳錶是一種實現起來很簡單,單層多指針的鏈表,它查找效率很高,堪比優化過的二叉平衡樹,且比平衡樹的實現,簡單的多的多。
typedef struct zskiplistNode {
robj *obj; //存儲對象的指針
double score; //分數
struct zskiplistNode *backward; //後退指針,每次只能退一步
struct zskiplistLevel {
struct zskiplistNode *forward; //前進指針,每次可以跳躍好幾步
unsigned int span; //這個就是決定前進指針能跳躍幾步的跨度標誌
} level[];
} zskiplistNode;
zskiplistNode是跳躍表的節點結構,obj指針指向存儲具體對象的地址,score標誌分數。
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
zskiplist持有節點,並記錄頭結點和尾節點以及長度,level記錄層數最大的節點的層數,也就是zskiplistNode中最大的level.size。
上圖來自於一本非常經典的書籍《Redis的設計與實現》。非常清晰的勾勒了跳躍表的數據結構,單看這個圖,就知道其查找效率高於鏈表。這是一種用空間來換時間的鏈表實現。
注:其層次的分配是隨機的,下面是其隨機生成的算法,非常簡單。
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
跳錶在Redis中僅僅作爲zset(有序集合)的底層實現出現,所以其數據結構定義在server.h中,其實現函數在t_zset.c中。
6.ziplist(壓縮表)
ziplist是一個編碼後的列表,是由一系列特殊編碼的連續內存塊組成的順序型數據結構,特殊的設計使得內存操作非常有效率,此列表可以同時存放字符串和整數類型,列表可以在頭尾各邊支持推加和彈出操作在O(1)常量時間,但是,因爲每次操作涉及到內存的重新分配釋放,所以加大了操作的複雜性 。
typedef struct zlentry {
//prevrawlen爲上一個數據結點的長度,prevrawlensize爲記錄該長度數值所需要的字節數
unsigned int prevrawlensize, prevrawlen;
//len爲當前數據結點的長度,lensize表示表示當前長度表示所需的字節數
unsigned int lensize, len;
//數據結點的頭部信息長度的字節數
unsigned int headersize;
//編碼的方式
unsigned char encoding;
//數據結點的數據(已包含頭部等信息),以字符串形式保存
unsigned char *p;
} zlentry;
zlentry是實際存儲數據的節點。一個ziplist可以有多個zlentry節點,具體形式如下:


壓縮表之所以成爲壓縮表,是因爲它起到了一定的壓縮功能,對於其他的數據結構爲了快速定位,使用了大量的指針結構,這樣對於長度較大的數據優勢明顯,但是對於長度非常小的數據,比如說一個表裏的每一個數據長度都很短,但是數據量並不小,這樣的話,就會出現大量的指針結構,造成內存浪費,而壓縮表則分配了一塊連續內存來存儲,就避免了大量的指針結構,節省了內存。另外,ziplist也使用了動態分配內存的方法,也一定程度上避免了內存的浪費。下圖(此圖來自書本)是內存的每塊代表的含義:


壓縮表在Redis中的應用只存在於hash和list結構的實現中,爲了在存儲時節省內存。
在Redis 中,這幾個數據結構算比較核心的了,大部分的功能需求都可以通過這幾個中的一個或多個實現。
推薦一本非常不錯的講解Redis源碼的書籍:黃健宏先生的《Redis 的設計與實現》,也是翻譯本,在中國算比較核心的講解Redis實現的書籍了(好像也沒有其他的)。

參考書籍:《Redis設計與實現》

特此聲明:本文非本人所原創,是轉載他人文章並總結整合方便自己學習的筆記!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章