概述
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:4bytes的无符号整数,表示ziplist占用的总字节数(即包括zlbytes和zlend的总字节数);
- zltail:4bytes的无符号整数,表示最后一个entry相对ziplist起始地址的偏移,通过该字段,redis无需遍历整个列表即可找到末元素;
- zllen:2个字节,ziplist包含的entry数量;由于2个字节最多表示65535,因此当元素数目大于65535时,需要遍历整个列表获取列表的entry数量;
- zlend:特殊值0xFF,表示ziplist的结束;
注意:上述字段都采用little endian;
entry的编码格式如下:
- 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;