Redis对外可见的5种数据结构

目录

RedisObject

Redis的编码方式 

对外可见的5种数据结构

1.string 

string结构的源码

为什么是小于44字节会采用embstr编码?

embstr和raw区别

2.list 

list结构的源码

 3.set

set结构的源码 

4.zset

zset结构的源码

5.hash 

hash结构的源码

Redis中key和value的对应关系 


RedisObject

Redis中任意数据类型的key和value都会被封装成一个RedisObject对象。其源码如下:

//server.h
/* The actual Redis Object */
#define OBJ_STRING 0    /* String object. */
#define OBJ_LIST 1      /* List object. */
#define OBJ_SET 2       /* Set object. */
#define OBJ_ZSET 3      /* Sorted set object. */
#define OBJ_HASH 4      /* Hash object. */

#define LRU_BITS 24

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
} robj;

redisObject的成员变量的含义如下图所示:

 为什么要对底层的数据结构进行一层包装?

通过封装, 可以根据对象的类型动态地选择存储结构和可以使用的命令, 实现节省空间和优化查询速度。

Redis的编码方式 

根据存储的数据类型的不同,Redis会选择不同的编码方式,共包含11中不同类型。

/* Objects encoding. Some kind of objects like Strings and Hashes can be
 * internally represented in multiple ways. The 'encoding' field of the object
 * is set to one of this fields for this object. */
#define OBJ_ENCODING_RAW 0     /* Raw representation */ //raw编码动态字符串
#define OBJ_ENCODING_INT 1     /* Encoded as integer */ //long类型的整数
#define OBJ_ENCODING_HT 2      /* Encoded as hash table */ //hash表(字典dict)
#define OBJ_ENCODING_ZIPMAP 3  /* Encoded as zipmap */  //编码为zipmap,已废弃
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */ //双端链表,在数据量少使用,这是旧版的编码方式
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */ //压缩列表
#define OBJ_ENCODING_INTSET 6  /* Encoded as intset */ //整数集合
#define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */ //跳表
#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */ //embstr的动态字符串
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */ //快速列表
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */ //stream流

对外可见的5种数据结构

结构体redisObject中的变量type就是对外可见的数据类型,有string,hash,list,setzset 5种。

Redis中会根据存储的数据类型不同,会选择不同的编码方式。用string来举个例子。

同样是存储string,其编码方式就会有不同。下图是每种结构类型会使用到的编码方式。

1.string 

Redis中key字符串类型的value最大限制均为512mbkey均是string类型

string是Redis中最常见的数据存储类型:

  • 其基本编码方式是raw,这是基于简单动态字符串(SDS)实现的。
  • 若存储的SDS长度小于44字节,则会采用embstr编码,此时的object head与SDS是一段连续空间
  • 存储的字符串是整数值,并且大小在LONG_MAX范围捏,则会采用int编码,直接将数据保存在RedisObject的ptr指针位置(刚好8字节),不再需要SDS。

string结构的源码

从string结构插入元素的代码入手:

/* SET key value [NX] [XX] [KEEPTTL] [GET] [EX <seconds>] [PX <milliseconds>]
 *     [EXAT <seconds-timestamp>][PXAT <milliseconds-timestamp>] */

//结构体client的变量argv是个数组,元素是robj*。所以客户端输入的数据是已经封装成redisObject结构体的了
//比如输入 set name jack,那name,jack都会封装成redisobject结构,name的赋值给c->argv[1],jack的会赋值给c->argv[2]
void setCommand(client *c) {
    robj *expire = NULL;
    int unit = UNIT_SECONDS;
    ...............................
    //尝试对字符串对象进行编码以节省空间,若字符串是整数的话,就进行编码转换
    c->argv[2] = tryObjectEncoding(c->argv[2]);
    ...........................
}

//object.c
/* Try to encode a string object in order to save space */
//该函数编码是embstr时候,为什么要释放整个redisObject对象?
//因为embstr编码的只申请一次内存空间,这是一个整块,之后是存储整数,就不需要那么大空间,就可释放该对象,再新创redisObject
//而raw编码的,是两次申请内存空间,是两块不连续的内存空间,所以释放ptr部分即可。
robj *tryObjectEncoding(robj *o) {
    long value;
    sds s = o->ptr;
    size_t len;

    .................................................
    len = sdslen(s);
    if (len <= 20 && string2l(s,len,&value)) {//函数string21,表明字符串s中存储的是整数
        if ((server.maxmemory == 0 ||
            !(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) &&
            value >= 0 &&
            value < OBJ_SHARED_INTEGERS)
        {
            .....................................
        } else {
            if (o->encoding == OBJ_ENCODING_RAW) {
                sdsfree(o->ptr);
                o->encoding = OBJ_ENCODING_INT;
                o->ptr = (void*) value;
                return o;
            } else if (o->encoding == OBJ_ENCODING_EMBSTR) {
                decrRefCount(o);        //释放o
                return createStringObjectFromLongLongForValue(value);//创建编码为OBJ_ENCODING_INT的redisObject对象
            }
        }
    }

    .................................
    return o; /* Return the original object. */
}

创建string的源码:

#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
robj *createStringObject(const char *ptr, size_t len) {
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
        return createEmbeddedStringObject(ptr,len); //小于44字节的,就使用embstr编码
    else
        return createRawStringObject(ptr,len);
}

robj *createEmbeddedStringObject(const char *ptr, size_t len) {
    robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);
    struct sdshdr8 *sh = (void*)(o+1);
    
    //给结构体redisObject的变量赋值
    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();
    }

    //给结构体sdshdr的属性赋值
    sh->len = len;
    sh->alloc = len;
    sh->flags = SDS_TYPE_8;
    if (ptr == SDS_NOINIT)
        sh->buf[len] = '\0';
    else if (ptr) {
        memcpy(sh->buf,ptr,len);
        sh->buf[len] = '\0';
    } else {
        memset(sh->buf,0,len+1);
    }
    return o;
}

/* Create a string object with encoding OBJ_ENCODING_RAW, that is a plain
 * string object where o->ptr points to a proper sds string. */
robj *createRawStringObject(const char *ptr, size_t len) {
    return createObject(OBJ_STRING, sdsnewlen(ptr,len));
}

robj *createObject(int type, void *ptr) {
    robj *o = zmalloc(sizeof(*o));
    o->type = type;
    o->encoding = OBJ_ENCODING_RAW;
    o->ptr = ptr;
    o->refcount = 1;

    ..................省略部分
    return o;
}

为什么是小于44字节会采用embstr编码?

  • 因为Redis底层的内存分配是统一管理的(用c语言的内存分配器jemalloc、tcmalloc),而分配的内存大小为2^{n},即是2,4,8等。
  • Redis认为一次分配的内存大于64字节是个得不偿失的行为,大内存的申请会消耗更长的时间等。所以在Redis中,超过64字节的内存分配,就会多次进行的
  • 前面我们学习的结构体RedisObject的大小是4bit+4bit+24bit+4字节+8字节=16字节。
  • 而字符串为空的SDS,其至少占用1(len)+1(alloc)+1(flags)+1('\0')=4字节。再加上对象头16字节,那一共是20字节。那剩余给字符串的就是64-20=44字节。所以实际字符串超过44字节则为长字符串,会转换为raw格式存储

embstr和raw区别

 embstr与raw都使用redisObject和sds保存数据。

  • embstr的使用只分配一次内存空间,因此其redisObject和sds是连续的;而raw需要分配两次内存空间,分别为其redisObject和sds分配空间。
  • 因此与raw相比,embstr的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便
  • 而embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,因此redis中的embstr实现为只读。

2.list 

Redis的list结构类似一个双端链表,可以从首/尾操作列表中的元素:

  • 在3.2版本前,Redis采用ziplist和linkedlist来实现list。当元素数量小于512并且元素的大小小于64字节时采用ziplist编码,超过则采用linkedlist编码。
  • 在3.2版本后,Redis统一采用quicklist来实现list。quicklist是个双端链表,每个节点是ziplist。(Redis7.0后,listpack已完全替换ziplist)

list结构的源码

从 list结构插入元素的源码入手:

/* LPUSH <key> <element> [<element> ...] */
void lpushCommand(client *c) {
    pushGenericCommand(c,LIST_HEAD,0);
}
/* Implements LPUSH/RPUSH/LPUSHX/RPUSHX. 
 * 'xx': push if key exists. */
void pushGenericCommand(client *c, int where, int xx) {
    int j;

    //判断每个插入的元素的长度
    for (j = 2; j < c->argc; j++) {
        if (sdslen(c->argv[j]->ptr) > LIST_MAX_ITEM_SIZE) {
            addReplyError(c, "Element too large");
            return;
        }
    }

    //尝试找到key对应的list
    robj *lobj = lookupKeyWrite(c->db, c->argv[1]);
    if (checkType(c,lobj,OBJ_LIST)) return; //检查类型是否正确
    //检查是否为空
    if (!lobj) {
        if (xx) {
            addReply(c, shared.czero);
            return;
        }

        //为空,就创建新的quikclist
        lobj = createQuicklistObject();
        quicklistSetOptions(lobj->ptr, server.list_max_ziplist_size,
                            server.list_compress_depth);
        dbAdd(c->db,c->argv[1],lobj);
    }

    ..............................................
}

//创建list结构
robj *createQuicklistObject(void) {
    quicklist *l = quicklistCreate();    //创建快速列表
    robj *o = createObject(OBJ_LIST,l);
    o->encoding = OBJ_ENCODING_QUICKLIST;
    return o;
}

 3.set

 set是Redis中的集合,不保证元素有序的,其元素唯一,查询效率高。

  • 为了查询效率高,那可以使用Dict;而唯一性,就可以先从Dict中查询,若没有查到就插入,否则不插入。这个就是HT编码(Dict),Dict中的key可以用来存储元素,value统一为null。
  • 当存储的所有数据都是整数,并且元素数量不超过参数set-max-intset-entries时,set会采用intset编码以节省内存。

set结构的源码 

从set插入元素部分代码开始:

//t_set.c
//sadd key value1 value2
void saddCommand(client *c) {
    robj *set;
    int j, added = 0;

    //尝试找到对应key的set,可以简单认为c->argv[1]就是key,c->argv[2]就是value1
    set = lookupKeyWrite(c->db,c->argv[1]);
    //检查类型是否正确
    if (checkType(c,set,OBJ_SET)) return;
    //如果为空
    if (set == NULL) {
        set = setTypeCreate(c->argv[2]->ptr); //创建set
        dbAdd(c->db,c->argv[1],set);
    }

    ....................
}

robj *setTypeCreate(sds value) {
    //判断value是不是整数类型
    if (isSdsRepresentableAsLongLong(value,NULL) == C_OK)
        return createIntsetObject();    //若是,则采用intset编码
    return createSetObject();    //否则采用默认编码,即是HT
}

robj *createSetObject(void) {
    dict *d = dictCreate(&setDictType,NULL);    //创建dict
    robj *o = createObject(OBJ_SET,d);
    o->encoding = OBJ_ENCODING_HT;
    return o;
}

robj *createIntsetObject(void) {
    intset *is = intsetNew();    //创建intset
    robj *o = createObject(OBJ_SET,is);
    o->encoding = OBJ_ENCODING_INTSET;
    return o;
}

4.zset

zset也就是sortedSet,其中每个元素都需要指定一个score值和member值。其特性:

  • 可以根据score值排序
  • memeber值必须唯一
  • 可以根据member查询score

 因此,zset底层数据结构必须满足键值存储键必须唯一可排序这几个要求。

那可以使用什么结构来表示zset呢?

  • 可以排序的,并且同时存储score和member值,那可以使用跳表skiplist
  • 根据key快速找到value,可以使用dict。key保存member值,value保存score。
  • 这两种数据结构会通过指针来共享相同元素的成员和分值,所以不会产生重复成员和分值,不会造成内存的浪费

 确实是使用这两个结构体。源码中有这个结构体zset,源码如下:

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

那zset可以不使用dict吗?从性能上来说不妥。

因为zset需要可以通过member得到其score。而skiplist是通过score得到member(score有序),复杂度是O(logN),而通过member查找score,复杂度O(n),从头到尾遍历

当元素数量不多时,dict和skiplist的优势不明显,而且会更耗内存。所以,zset还会采用ziplist结构来节省内存,不过需要同时满足两个条件:

  • 元素数量小于zset_max_ziplist_entries,默认值是128
  • 每个元素的长度都小于zset_max_ziplist_value字节,默认值是64

zset结构的源码

从zset插入元素的代码入手: 

//t_zset.c
void zaddCommand(client *c) {
    zaddGenericCommand(c,ZADD_IN_NONE);
}

/* This generic command implements both ZADD and ZINCRBY. */
//zadd key score1 member1 score2 memeber2 .......
void zaddGenericCommand(client *c, int flags) {
    robj *key = c->argv[1];
    robj *zobj;

    ...................................

    //查找key对应的zset
    zobj = lookupKeyWrite(c->db,key);
    //若没有,就创建
    if (zobj == NULL) {
        if (xx) goto reply_to_client; /* No key + XX option: nothing to do. */
        if (server.zset_max_ziplist_entries == 0 ||
            server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr))
        {
            zobj = createZsetObject();
        } else {
            zobj = createZsetZiplistObject();
        }
        dbAdd(c->db,key,zobj);
    }
    
    //插入元素部分
    for (j = 0; j < elements; j++) {
        double newscore;
        score = scores[j];
        int retflags = 0;

        ele = c->argv[scoreidx+1+j*2]->ptr;
        //zsetadd插入元素,如果最初是ziplist,但随着元素数量的增加,就可能需要进行编码转换,即是用其他结构来保存
        int retval = zsetAdd(zobj, score, ele, flags, &retflags, &newscore);
    }
    ..............................
}

//创建zset
robj *createZsetObject(void) {
    zset *zs = zmalloc(sizeof(*zs));
    robj *o;

    zs->dict = dictCreate(&zsetDictType,NULL);    //创建dict
    zs->zsl = zslCreate();        //创建skiplist
    o = createObject(OBJ_ZSET,zs);    //给redisObject的type赋值为obj_zset
    o->encoding = OBJ_ENCODING_SKIPLIST;    //zset的编码是skiplist
    return o;
}

robj *createZsetZiplistObject(void) {
    unsigned char *zl = ziplistNew();
    robj *o = createObject(OBJ_ZSET,zl);
    o->encoding = OBJ_ENCODING_ZIPLIST;
    return o;
}

zset结构的编码转换是在插入时候进行判断的,在函数zsetAdd中。

int zsetAdd(robj *zobj, double score, sds ele, int in_flags, int *out_flags, double *newscore) {
      .................................

    //判断编码方式,若是ziplist编码,就可能需要进行编码转换
    /* Update the sorted set according to its encoding. */
    if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
        unsigned char *eptr;
        //判断当前元素是否存在,已存在则更新score即可
        if ((eptr = zzlFind(zobj->ptr,ele,&curscore)) != NULL) {
            ...........................................
            return 1;
        } else if (!xx) {
            /* check if the element is too large or the list
             * becomes too long *before* executing zzlInsert. */
            //元素不存在,则新增,需要先判断ziplist长度是否超标,元素的大小是否已超
            if (zzlLength(zobj->ptr)+1 > server.zset_max_ziplist_entries ||
                sdslen(ele) > server.zset_max_ziplist_value ||
                !ziplistSafeToAdd(zobj->ptr, sdslen(ele)))
            {
                //表示超出了,则需要转换为skiplist编码
                zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);
            } else {
                //没有超出,就直接往ziplist插入
                zobj->ptr = zzlInsert(zobj->ptr,ele,score);
                if (newscore) *newscore = score;
                *out_flags |= ZADD_OUT_ADDED;
                return 1;
            }
        } else {
            *out_flags |= ZADD_OUT_NOP;
            return 1;
        }
    }

    //本身就是skiplist编码,无需进行转换,就插入即可
    if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
        ..................................
    } else {
        serverPanic("Unknown sorted set encoding");
    }
    return 0; /* Never reached. */
}

 而ziplist本身没有排序功能,而且没有键值对的概念,因此需要由zset通过编码来实现:

  • ziplist是连续内存,所以score和member是紧挨在一起的两个entry,member在前,score在后
  • score越小越接近队首,score越大越接近队尾,按照score值升序排列

查看编码示例 

5.hash 

这个很明显是用dict。所以,hash采用的编码与zset也基本一致的,只需把排序相关的skiplist去掉即可。

而hash结构默认使用ziplist编码,这是为了节省内存。ziplist中相邻的两个entry分别保存field和value。

当数据量较大时,hash结构会转换成HT编码,即是dict,触发条件有2个:

  • ziplist中的元素个数超过了hash-max-ziplist-entries,默认512。
  • ziplist中任意entry大小超过了hash-max-ziplist-value,默认64字节。

hash结构的源码

从hash结构的插入元素的代码入手:

//命令格式例子:hset key filed value [filed value]...
void hsetCommand(client *c) {
    int i, created = 0;
    robj *o;
    .................................

    //1. 判断hash的key是否存在,若不存在就创建新的,默认是采用ziplist编码
    if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
     //2. 判断是否需要把ziplist编码转换为dict编码
    hashTypeTryConversion(o,c->argv,2,c->argc-1);  
    //3. 遍历每一对field和value,并执行hset命令
    for (i = 2; i < c->argc; i += 2)
        created += !hashTypeSet(o,c->argv[i]->ptr,c->argv[i+1]->ptr,HASH_SET_COPY);

    ..................................
}

1.判断key是否存在,若不存在就创建hash

robj *hashTypeLookupWriteOrCreate(client *c, robj *key) {
    robj *o = lookupKeyWrite(c->db,key);    //查找是否有该key
    if (checkType(c,o,OBJ_HASH)) return NULL;   //检查编码类型

    if (o == NULL) {
        o = createHashObject(); //不存在就创建新的
        dbAdd(c->db,key,o);
    }
    return o;
}

//从代码可见,其默认编码是ziplist
robj *createHashObject(void) {
    unsigned char *zl = ziplistNew();   //创建ziplist
    robj *o = createObject(OBJ_HASH, zl);
    o->encoding = OBJ_ENCODING_ZIPLIST;
    return o;
}

2.判断是否需要将编码转换成HT编码

/* Check the length of a number of objects to see if we need to convert a
 * ziplist to a real hash. Note that we only check string encoded objects
 * as their string length can be queried in constant time. */
void hashTypeTryConversion(robj *o, robj **argv, int start, int end) {
    int i;
    size_t sum = 0;
    //若编码不是ziplist,表明不用进行转换,立即返回
    if (o->encoding != OBJ_ENCODING_ZIPLIST) return;
    //遍历命令中的field、value参数,即是遍历argv[i]
    for (i = start; i <= end; i++) {
        if (!sdsEncodedObject(argv[i])) //如果编码不是OBJ_ENCODING_RAW和OBJ_ENCODING_embstring
            continue;
        size_t len = sdslen(argv[i]->ptr);
        //若果field或value超过了hash_max_ziplist_value,则需转换,转成HT编码(dict)
        if (len > server.hash_max_ziplist_value) {
            hashTypeConvert(o, OBJ_ENCODING_HT);
            return;
        }
        sum += len;
    }
    if (!ziplistSafeToAdd(o->ptr, sum)) //若ziplist大小超过1G,也转成HT编码
        hashTypeConvert(o, OBJ_ENCODING_HT);
}

/* Don't let ziplists grow over 1GB in any case, don't wanna risk overflow in
 * zlbytes*/
#define ZIPLIST_MAX_SAFETY_SIZE (1<<30)
int ziplistSafeToAdd(unsigned char* zl, size_t add) {
    size_t len = zl? ziplistBlobLen(zl): 0;
    if (len + add > ZIPLIST_MAX_SAFETY_SIZE)
        return 0;
    return 1;
}

//进行编码转换,重点就在hashTypeConvertZiplist函数中
void hashTypeConvert(robj *o, int enc) {
    if (o->encoding == OBJ_ENCODING_ZIPLIST) {
        hashTypeConvertZiplist(o, enc);
    } else if (o->encoding == OBJ_ENCODING_HT) {
        serverPanic("Not implemented");
    } else {
        serverPanic("Unknown hash encoding");
    }
}

void hashTypeConvertZiplist(robj *o, int enc) {
    if (enc == OBJ_ENCODING_ZIPLIST) {
        /* Nothing to do... */

    } else if (enc == OBJ_ENCODING_HT) {//需要转换为HT编码的
        hashTypeIterator *hi;
        dict *dict;
        int ret;

        hi = hashTypeInitIterator(o);
        dict = dictCreate(&hashDictType, NULL);//创建dict

        //逐一把key,value赋值给新创建的dict
        while (hashTypeNext(hi) != C_ERR) {
            sds key, value;
            //获取key,value
            key = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_KEY);
            value = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_VALUE);
            ret = dictAdd(dict, key, value);    //往dict中添加
            if (ret != DICT_OK) {
                serverLogHexDump(LL_WARNING,"ziplist with dup elements dump",
                    o->ptr,ziplistBlobLen(o->ptr));
                serverPanic("Ziplist corruption detected");
            }
        }
        hashTypeReleaseIterator(hi);
        zfree(o->ptr);
        o->encoding = OBJ_ENCODING_HT;  //更新编码为HT编码
        o->ptr = dict;              //redisObject对象的ptr指针指向dict
    } 
}

 3.for循环遍历中,将field和value进行设置插入:

int hashTypeSet(robj *o, sds field, sds value, int flags) {
    int update = 0;
    //判断编码方式
    if (o->encoding == OBJ_ENCODING_ZIPLIST) {  //若是ziplist编码
        unsigned char *zl, *fptr, *vptr;

        zl = o->ptr;
        fptr = ziplistIndex(zl, ZIPLIST_HEAD);//找到head位置
        if (fptr != NULL) {//head不为空,开始查找field
            fptr = ziplistFind(zl, fptr, (unsigned char*)field, sdslen(field), 1);//查找field
            if (fptr != NULL) { //表明该field已存在,即是需要进行更新
                /* Grab pointer to the value (fptr points to the field) */
                vptr = ziplistNext(zl, fptr);//到达下一节点位置,即是value
                serverAssert(vptr != NULL);
                update = 1;

                /* Replace value */ //更新该位置的值value
                zl = ziplistReplace(zl, vptr, (unsigned char*)value,
                        sdslen(value));
            }
        }

        if (!update) {  //不存在,则直接push
            /* Push new field/value pair onto the tail of the ziplist */
            //依次将新的field和value都push到ziplist的尾部
            zl = ziplistPush(zl, (unsigned char*)field, sdslen(field),
                    ZIPLIST_TAIL);
            zl = ziplistPush(zl, (unsigned char*)value, sdslen(value),
                    ZIPLIST_TAIL);
        }
        o->ptr = zl;

        /* Check if the ziplist needs to be converted to a hash table */
        //插入了新元素,判断ziplist长度是否超过了,超过则转为HT编码
        if (hashTypeLength(o) > server.hash_max_ziplist_entries)
            hashTypeConvert(o, OBJ_ENCODING_HT);
    } else if (o->encoding == OBJ_ENCODING_HT) {   //若是HT编码,直接插入或更新
        dictEntry *de = dictFind(o->ptr,field);
        ..................................
    } else {
        serverPanic("Unknown hash encoding");
    }

    ......................
    return update;
}

Redis中key和value的对应关系 

首先,Redis内部会将key和value都封装成对应的redisObject对象。

  • 其中key是string数据类型的redisOect
  • value的redisObject就根据用户使用插入命令时候设置的。比如set结构类型的插入,sadd name jack。jack就会封装成type是set的redisObject。

那key是怎么对应上该value,其是如何可以快速通过key找到value的呢?

其实,Redis也是使用一个数据结构来存放key和value。要想查找效率高,那就是使用dict数据结构。所以key和value是通过hash表进行关联的。每个哈希表都有一个键值对,其key是唯一的,并且与对应的value相关联。

所以说,整个Redis是个大的dict。我们通过string的获取来查看下,比如命令get age。这时redis会调用getCommand函数。该函数最终会调用db.c文件的lookupKey函数,该函数内部是从dict中查找该key。

//命令格式 get key
void getCommand(client *c) {
    getGenericCommand(c);
}

//其重点就在lookupKeyReadOrReply函数中
int getGenericCommand(client *c) {
    robj *o;
     //查找是否有该key
    if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.null[c->resp])) == NULL)
        return C_OK;

    if (checkType(c,o,OBJ_STRING)) {
        return C_ERR;
    }
    addReplyBulk(c,o);
    return C_OK;
}

robj *lookupKeyReadOrReply(client *c, robj *key, robj *reply) {
    robj *o = lookupKeyRead(c->db, key);
    if (!o) SentReplyOnKeyMiss(c, reply);
    return o;
}

robj *lookupKeyRead(redisDb *db, robj *key) {
    return lookupKeyReadWithFlags(db,key,LOOKUP_NONE);
}

robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) {
    robj *val;

   ................
    val = lookupKey(db,key,flags);//查找该key
    .......................
    return val;

    ..........................
}

//db.c
robj *lookupKey(redisDb *db, robj *key, int flags) {
    dictEntry *de = dictFind(db->dict,key->ptr);    //在dict中查找
    if (de) {
        robj *val = dictGetVal(de);

        if (!hasActiveChildProcess() && !(flags & LOOKUP_NOTOUCH)){
            if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
                updateLFU(val);
            } else {
                val->lru = LRU_CLOCK();
            }
        }
        return val;
    } else {
        return NULL;
    }
}
  • 24
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值