redis数据结构

概述
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的实现:

  1. Java中HashMap的底层存储采用数组,每个数组里面的元素实际上是链表的头节点;往Map里面添加元素时,首先根据key计算出数组下标,然后将节点添加到数组下标指定的链表头部;
  2. 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;

你可能感兴趣的:(redis数据结构)