概述
redis是目前最常用的高效緩存系統,在互聯網行業中使用廣泛;因此打算了解下其內部采用的數據結構;
redisObject
redis使用redisObject表示鍵值;結構如下:
typedef struct redisObject {
unsigned type:4;//對象類型,可以通過type命令查看
unsigned encoding:4;//對象編碼,可以通過object encoding命令查看
unsigned lru:24; //如果采用LRU策略,記錄相對于 server.lruclock的時間,如果采用LFU,低8bit記錄頻率,高16bit記錄時間;可以通過object idletime命令查看
int refcount;//引用數,如果為0需要釋放內存;可以通過object refcount命令查看
void *ptr; //指針,指向具體的值
} robj;
其中type表示redis支持的數據類型:
- String
- List
- Set
- SortedSet
- Hash
encoding表示對象的編碼格式:
- ENCODING_RAW
- ENCODING_INT
- ENCODING_HT
- ENCODING_ZIPMAP
- ENCODING_ZIPLIST
- ENCODING_INTSET
- ENCODING_SKIPLIST
- ENCODING_EMBSTR
- ENCODING_QUICKLIST
對象類型和編碼方式的對應關系為:
對象類型 | 編碼方式 |
---|---|
String | ENCODING_RAW、ENCODING_EMBSTR、ENCODING_INT |
List | ENCODING_QUICKLIST |
Set | ENCODING_INTSET、ENCODING_HT |
SortedSet | ENCODING_SKIPLIST, ENCODING_ZIPLIST |
Hash | ENCODING_ZIPLIST,ENCODING_HT |
根據不同的類型(type),redis會采用不同的結構,通過ptr進行引用;
下面具體分析Redis提供的幾種數據類型:
字符串
當對象為字符串即redisObject的type為String時,encoding可采用ENCODING_RAW、ENCODING_EMBSTR或ENCODING_INT;那么redis是如何確定encoding的呢?
if (len <= 20 && string2l(s,len,&value)) {//如果字符串可以轉換為數字,則使用ENCODING_INT編碼
if ((server.maxmemory == 0 ||
!(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) &&
value >= 0 &&
value < OBJ_SHARED_INTEGERS)
{//如果滿足條件,優先使用共享整數節約內存;
decrRefCount(o);
incrRefCount(shared.integers[value]);
return shared.integers[value];
} else {//如果設置了maxmemory而且內存過期策略采用LRU或LFU,則不使用共享整數,因為這個時候,要求每個對象有自己的lru信息供LRU或LFU算法使用
if (o->encoding == OBJ_ENCODING_RAW) sdsfree(o->ptr);
o->encoding = OBJ_ENCODING_INT;
o->ptr = (void*) value;
return o;
}
}
//字符串長度小于44,使用ENCODING_EMBSTR,否則使用RAW
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) {
robj *emb;
if (o->encoding == OBJ_ENCODING_EMBSTR) return o;
emb = createEmbeddedStringObject(s,sdslen(s));
decrRefCount(o);
return emb;
}
可以看到當encoding為ENCODING_INT時,redisObject的ptr指向long;而當采用ENCODING_EMBSTR和ENCODING_RAW時,ptr指向的都是SDS對象;
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /*低位3bit表示SDS_TYPE,其余5bit表示長度 */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* buf數組中已使用的字節長度 */
uint8_t alloc; /* 預分配字節數,不包括'\0' */
unsigned char flags; /* 低位3bit表示SDS_TYPE,其余5bit未使用*/
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* buf數組中已使用的字節長度 */
uint16_t alloc; /* 預分配字節數,不包括'\0'*/
unsigned char flags; /* 低位3bit表示SDS_TYPE,其余5bit未使用 */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* buf數組中已使用的字節長度 */
uint32_t alloc; /* 預分配字節數,不包括'\0' */
unsigned char flags; /* 低位3bit表示SDS_TYPE,其余5bit未使用*/
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* buf數組中已使用的字節長度 */
uint64_t alloc; /* 預分配字節數,不包括'\0' */
unsigned char flags; /* 低位3bit表示SDS_TYPE,其余5bit未使用 */
char buf[];
};
可以看到,redis為不同長度的字符串定義了不同的結構體;另外為了可以繼續使用標準C的字符串函數,buf以'\0'結尾;
ENCODING_EMBSTR和ENCODING_RAW有什么區別呢?
//兩次分配內存,分別為redisObject和SDS對象分配內存
robj *createRawStringObject(const char *ptr, size_t len) {
return createObject(OBJ_STRING, sdsnewlen(ptr,len));
}
robj *createEmbeddedStringObject(const char *ptr, size_t len) {
//一次性申請redisObject和SDS的內存
robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);
struct sdshdr8 *sh = (void*)(o+1);
o->type = OBJ_STRING;
o->encoding = OBJ_ENCODING_EMBSTR;
o->ptr = sh+1;
o->refcount = 1;
if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
} else {
o->lru = LRU_CLOCK();//
}
sh->len = len;
sh->alloc = len;
sh->flags = SDS_TYPE_8;
if (ptr) {
memcpy(sh->buf,ptr,len);
sh->buf[len] = '\0';
} else {
memset(sh->buf,0,len+1);
}
return o;
}
List
當redisObject的type為List時,表示列表對象;根據前面的說明,List的encoding采用ENCODING_QUICKLIST;簡單來說quicklist是由ziplist為節點組成的list;
quicklist結構定義如下:
typedef struct quicklist {
quicklistNode *head;//頭節點
quicklistNode *tail;//尾節點
unsigned long count; //列表元素數目
unsigned int len; //quicklistNode數目
int fill : 16; /* fill factor for individual nodes */
unsigned int compress : 16; //從尾節點算起,不壓縮節點的數目;
} quicklist;
typedef struct quicklistNode {
struct quicklistNode *prev;//前節點
struct quicklistNode *next;//后節點
unsigned char *zl;// 指向ziplist的指針
unsigned int sz; /* ziplist占用字節 */
unsigned int count : 16; /* ziplist中的元素數目 */
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
unsigned int recompress : 1; //是否被壓縮過
unsigned int extra : 10; //預留字段,可用于后續擴展
} quicklistNode;
Set
當redisObject的type為Set時,表示Set對象;根據前面的說明,Set的encoding采用ENCODING_INTSET和ENCODING_HT;如果Set中的每個元素都是數字則采用ENCODING_INTSET,否則采用ENCODING_HT編碼;
Set可以采用ENCODING_HT編碼,這個很好理解,就像Java中的HashSet采用HashMap實現一樣;這邊單獨介紹下intset:
typedef struct intset {
uint32_t encoding;//數組元素編碼,見下文說明
uint32_t length;//數組元素個數
int8_t contents[];
} intset;
可以看到intset實際上就是個數組,它要求set中的元素都是數字,而且每個元素占用的內存空間都是固定的,通過encoding來確定:
- INTSET_ENC_INT16:2個字節,范圍為-32768~32767
- INTSET_ENC_INT32:4個字節,范圍為-2147483648~2147483647
- INTSET_ENC_INT64:8個字節,范圍為-9223372036854775808~9223372036854775807
注意:intset中的元素是按照從小到大的順序排列的,不允許出現重復;
有人可能有疑問,為什么數組的類型是int8_t?這是因為如果元素占用兩個字節,則可以用兩個數組元素表示;如果占用4個字節,則用4個數組元素表示;因此contents的長度除以每個元素占用的字節數才是intset的真正長度;另外由于整個intset采用同一編碼,因此即使intset中只有一個元素比較大(8個字節),其它元素都比較小(2個字節),仍然要采用8個字節表示每個元素,這時內存會存在浪費;
SortedSet
SortedSet的encoding采用ENCODING_SKIPLIST或ENCODING_ZIPLIST;
redis.conf文件中有兩個配置項:
- zset-max-ziplist-entries:當元素數目超出該配置項的值,不能使用ziplist編碼,默認為128;
- zset-max-ziplist-value:當單個元素占用字節數超出該配置項的值,不能使用ziplist編碼,默認為64;
ziplist編碼
ziplist編碼格式如下:
<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
- zlbytes:4bytes的無符號整數,表示ziplist占用的總字節數(即包括zlbytes和zlend的總字節數);
- zltail:4bytes的無符號整數,表示最后一個entry相對ziplist起始地址的偏移,通過該字段,redis無需遍歷整個列表即可找到末元素;
- zllen:2個字節,ziplist包含的entry數量;由于2個字節最多表示65535,因此當元素數目大于65535時,需要遍歷整個列表獲取列表的entry數量;
- zlend:特殊值0xFF,表示ziplist的結束;
注意:上述字段都采用little endian;
entry的編碼格式如下:
<prevlen> <encoding> <entry-data>
- prevlen:前一個entry占用的字節數,可用于從末entry節點遍歷ziplist;根據前entry節點長度的不同,占用1或5bytes;如果前entry節點長度小于等于254,prevlen占用1bytes;否則會占用5bytes,并且第一個bytes會被設置為0xFF,后4bytes表示長度;
- encoding:由于entry-data可能為數字也可能為字符,而且占用的字節數也不一樣,因此通過encoding來區分;
下面具體來看看encoding的定義:
- |00pppppp|
1字節,用6bit表示字符串的長度; - |01pppppp|qqqqqqqq|
2字節,用14bit表示字符串的長度,采用big endian; - |10000000|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt|
5字節,第一個字節的剩余6bit為0,不使用;剩余4字節表示字符串的長度,采用big endian; - |11000000|
3字節,用剩余2字節表示整數; - |11010000|
5字節,用剩余4字節表示整數; - |11100000|
9字節,用剩余8字節表示整數; - |11110000|
4字節,用剩余3字節表示整數; - |11111110|
2字節,用剩余1字節表示整數; - |1111xxxx|
表示0~12的整數;由于11110000和11111110前面被使用了,因此xxxx表示的是1~13,需要將其減去1得到表示的整數值;
注意:上述整數都采用little endian;
從上述介紹可以看出實際上ziplist是個雙向鏈表,可以從表頭遍歷查找,也可從表尾遍歷查找;
skiplist編碼
當采用skiplist編碼時,redisObject的ptr指向的是zset:
typedef struct zset {
dict *dict;//key為sortedset元素,value為score
zskiplist *zsl;//指向跳躍列表
} zset;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;//頭尾指針
unsigned long length;//節點數量
int level;//表中節點的最大層數
} zskiplist;
typedef struct zskiplistNode {
sds ele;//sortedset元素
double score;//分值
struct zskiplistNode *backward;//后退指針,用于從從表尾往表頭訪問節點
struct zskiplistLevel {
struct zskiplistNode *forward;//前向指針,用于從表頭往表尾訪問節點
unsigned int span;//記錄當前節點與前向指針指向節點的跨度,即相距幾個節點;
} level[];
} zskiplistNode;
可以看到由于存在level,節點查找時,不用逐個節點查找,而是可以跳躍查找,因此查找速度會更快;
Hash
Hash的encoding采用ENCODING_ZIPLIST或ENCODING_HT,關于ziplist,在SortedSet中已經介紹,只不過Hash會用相鄰的兩個entry表示key和value;
下述兩個變量會影響Hash的編碼:
- hash-max-ziplist-entries:默認為512;如果元素數目大于該值,需要采用ENCODING_HT編碼;
- hash-max-ziplist-value:默認為64;如果單個元素占用字節數大于該值,需要采用ENCODING_HT編碼;
下面看看ENCODING_HT,關于Hash,可以對比Java中HashMap的實現:
- Java中HashMap的底層存儲采用數組,每個數組里面的元素實際上是鏈表的頭節點;往Map里面添加元素時,首先根據key計算出數組下標,然后將節點添加到數組下標指定的鏈表頭部;
- redis的Hash實現也類似,但在幾個地方進行了優化:
typedef struct dictht {
dictEntry **table;//哈希表數組
unsigned long size;//哈希表大小
unsigned long sizemask;//哈希表大小掩碼,用于計算索引值,總是等于size-1
unsigned long used;//該哈希表以有節點數量
} dictht;
typedef struct dict {
dictType *type;//類型,不同類型dict可以有不同的計算索引哈希函數實現
void *privdata;
dictht ht[2];
long rehashidx; //rehash索引, -1表示不在rehash
unsigned long iterators;
} dict;
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;//無符號整數,
int64_t s64;//有符號數
double d;
} v;
struct dictEntry *next;//下個entry指針
} dictEntry;
- dict中包含兩個ht元素,這兩個ht是用于rehash,所謂的rehash也就是哈希表擴容或縮容;像Java中是同步進行的,而redis中為了提高響應時間,采用的是異步的方式,將元素從ht[0]遷移到ht[1];
- redis實現了更豐富的哈希函數,例如MurmurHash2;