Redis五大基础数据类型详解

Redis五大基础数据类型

redis 基础数据类型有5种: string、set、zset、list和hash。

String数据类型

string类型是redis中最常见的数据类型,其内部存在三种编码:EMBSTRRAWINT编码。

RAW编码

  • 基本编码方式是RAW,基于简单动态字符串(SDS)实现,存储上限为512mb
    在这里插入图片描述
    下方是sds其中一种数据结构。详情可去看笔者的另一篇文章:Redis数据结构系列一:动态字符串SDS
    struct __attribute__ ((__packed__)) sdshdr8 {
        uint8_t len; /* used:已保存的字符串字节数,不包括结束符。表示字节数最大长度8个bit,也就是2^8 -1 */
        uint8_t alloc; /* buf申请的总字节数,不包括结束符*/
        unsigned char flags; /* 不同SDS 头类型(sdshdr5|sdshdr8|sdshdr16|sdshdr32|sdshdr64), 用来控制SDS 头的大小*/
        char buf[];
    };
    
    在上图中的数据,总字节数len=50,可用flags=sdshdr8编码格式保存。

EMBSTR编码

  • 如果存储的SDS长度小于44字节,则会采用EMBSTR编码,此时RedisObject header与SDS是一段连续空间。申请内存时只需要调用一次内存分配函数,效率更高。

    在这里插入图片描述

    为什么RedisObject header和sds会变成连续空间?

    1. 由于上图中的数据,总字节数len=44,可用flags=sdshdr8编码格式保存。

    2. sds 占用的总字节数totalSdsBytes = len:1 + alloc:1 + flags:1 + 44 + 1(结束标识) = 48byte

    3. RedisObject header固定为16byte, RedisObject header + totalSdsBytes = 16 + 48 = 64byte正好是8k,即一个Redis内存页,没有内存碎片。同时只需申请一次内存,减少用户态和内核态的交互,提交效率。若总字节数len>44byte,会导致整个sds>48byte,就会转成RAW编码。

INT编码

  • 如果string类型保存的是LONG_MAX(2^32)范围内的数字,即采用INT编码

  • 若采用的是INT 编码,则使用*ptr 指针存储二进制数据(*ptr占用8byte,64bit,足以存储long_max的2^32大小的数据),去除sds结构,更加节省内存空间。

    其数据结构如下:
    在这里插入图片描述
    redis实践如下:
    在这里插入图片描述

String总结:

  • 使用string类型存储数据时,尽量不超过44byte,即内部采用embstr编码;

  • 使用string类型存储数据时,尽量将数据转为数字,即内部采用int编码;

List数据类型

List类型可以从首、尾操作列表中的元素

有以下类型满足List数据类型的特征:

  • LinkedList:普通链表,可以从双端访问,但内存占用较高、内存碎片较多,不是连续内存

  • ZipList:压缩链表,可以从双端访问,连续内存,内存占用低,但是存储上限低

  • QuickList:等于LinkedList+QuickList,可以从双端访问,内存占用低,包含多个ZipList,存储上限高

所以:Redis 3.2版本之后,都采用QuickList作为List数据类型的底层结构。 结构如下:

在这里插入图片描述

List源码分析

lpushxCommand命令,进入查看发现所有的操作命令底层都是调用的pushGenericCommand

void lpushxCommand(client *c) {
    pushGenericCommand(c,LIST_HEAD,1);
}

进入pushGenericCommand命令:

/**
 * client *c: 客户端连接redis服务器时,会将客户端的信息及输入的命令都封装成client对象
 * where:插入的位置,head|tail
 * xx:当xx=true时,代表key存在时,才push;当xx=false时,不存在则直接return。 默认为false
*/
void pushGenericCommand(client *c, int where, int xx) {
    int j;
    /**
     * lpush key1 v1 v2 v3; 
     * argv[0] = lpush
     * argv[1] = key1
     * argv[2] = v1
     * ...
    */
   // 1.判断输入的参数个数是否超出上限
    for (j = 2; j < c->argc; j++) {
        // 因为redis所有的键和值都会封装成RedisObject,其真实数据实际上是*ptr指向的地址,所以执行sdslen(c->argv[j]->ptr)获取数据长度
        if (sdslen(c->argv[j]->ptr) > LIST_MAX_ITEM_SIZE) {
            addReplyError(c, "Element too large");
            return;
        }
    }

    // 2.根据client中的db和key查询是否存在的RedisObject(robj是RedisObject的缩写)
    robj *lobj = lookupKeyWrite(c->db, c->argv[1]);
    // 3.判断lobj类型是否为LIST类型,若不是则直接返回
    if (checkType(c,lobj,OBJ_LIST)) return;
    if (!lobj) {
        // 4.1如果lobj不存在,且xx=true,代表key不存在则直接return,不执行push
        if (xx) {
            addReply(c, shared.czero);
            return;
        }
        // 4.2如果lobj不存在,且xx=false,代表需要创建新的List RedisObject
        // 在此处可见实际上创建的是 Quicklist
        lobj = createQuicklistObject();
        // 4.2.1 设置lobj中quicklist的属性
        // list_max_ziplist_size: 限制ziplist的最大长度
        // list_compress_depth: qucikList 头尾节点不压缩数量
        quicklistSetOptions(lobj->ptr, server.list_max_ziplist_size,
                            server.list_compress_depth);
        // 4.3 将创建的robj赋值给key
        dbAdd(c->db,c->argv[1],lobj);
    }

    for (j = 2; j < c->argc; j++) {
        // 5.依次将数据保存进list orbj
        listTypePush(lobj,c->argv[j],where);
        server.dirty++;
    }
    ... // 略
}

上述代码都已添加注释,可看出List数据类型最终创建的还是quicklist结构

Set数据类型

set数据类型的特点:

  1. 不保证数据有序

  2. 保证元素唯一(可以判断元素是否存在。添加SADD、判断是否存在SISMEMBER和差集SINTER命令中**都需要判断元素是否存在 **)

  3. 便于求交集、并集和差集

由于底层数据结构中快速查找和唯一仅有skipList和dict数据结构,而skipList中是由数值score来做排序,而set中不一定全是数值,所以只能选用dict数据结构

Set编码

  • 为了查询效率和唯一性,set采用HT编码(Dict)。dict的key用来存储元素,value统一为null。

  • 若set保存的全是数值类型,且不超过set-max-intset-entries时,Set会采用IntSet编码,节省内存。

robj *setTypeCreate(sds value) {
    // 若存储的数据都是整数,创建Inset编码的robj
    if (isSdsRepresentableAsLongLong(value,NULL) == C_OK)
        return createIntsetObject();
    // 若存储的数据不全是整数,创建hash-table(dict类型)编码的robj
    return createSetObject();
}
robj *createIntsetObject(void) {
    intset *is = intsetNew();
    robj *o = createObject(OBJ_SET,is);
    o->encoding = OBJ_ENCODING_INTSET;
    return o;
}
// 默认创建OBJ_ENCODING_HT编码的set,即dict类型
robj *createSetObject(void) {
    dict *d = dictCreate(&setDictType,NULL);
    robj *o = createObject(OBJ_SET,d);
    o->encoding = OBJ_ENCODING_HT;
    return o;
}
/** 向set添加元素,若元素存在直接返回;若不存在则在相应编码的结构中添加*/
int setTypeAdd(robj *subject, sds value) {
    long long llval;
    // 若是dict类型
    if (subject->encoding == OBJ_ENCODING_HT) {
        dict *ht = subject->ptr;
        // 直接添加元素到新的dictEntry中
        dictEntry *de = dictAddRaw(ht,value,NULL);
        if (de) {
            dictSetKey(ht,de,sdsdup(value));
            dictSetVal(ht,de,NULL);
            return 1;
        }
    } else if (subject->encoding == OBJ_ENCODING_INTSET) {
        // 若是intset类型
        // 先判断value是否是整数
        if (isSdsRepresentableAsLongLong(value,&llval) == C_OK) {
            uint8_t success = 0;
            // 若是整数,使用intsetAdd添加到set
            subject->ptr = intsetAdd(subject->ptr,llval,&success);
            if (success) {
                // 若添加成功,最后还需要判断是否大于set_max_intset_entries
                size_t max_entries = server.set_max_intset_entries;
                if (max_entries >= 1<<30) max_entries = 1<<30;
                // 若intset的entry数量>set_max_intset_entries,则转换编码为HT
                if (intsetLen(subject->ptr) > max_entries)
                    setTypeConvert(subject,OBJ_ENCODING_HT);
                return 1;
            }
        } else {
            // 若不是整数,则转换编码为HT
            setTypeConvert(subject,OBJ_ENCODING_HT);
            // 使用dictAdd添加元素到set
            serverAssert(dictAdd(subject->ptr,sdsdup(value),NULL) == DICT_OK);
            return 1;
        }
    } else {
        serverPanic("Unknown set encoding");
    }
    return 0;
}
  • Set中保存的**{5, 10, 20}**,结构如下:
    在这里插入图片描述

  • Set中保存的**{5, 10, 20, m1}**,结构如下:

在这里插入图片描述

ZSet数据类型

ZSet数据类型的特点:zset也就是sortedSet,其中每一个元素都需要指定一个member和score:

  • 可以根据score排序

  • member必须唯一

  • 可以根据member查询分数

ZSet底层数据结构必须满足:键值存储、键必须唯一、可排序和能根据key找到value这几个需求。(此时·key=member,score=value·

  • SkipList:可排序,可同时存储score和element值(member)。(1.不能做到key唯一;2.不能做到通过key查找value)

  • HT(Dict):可以存储键值,可以根据key找到value。(1.不能通过score快速定位range;2.不能排序)

由于无法使用一种数据结构完成zset类型的封装,所以redis采用dict +skiplist 类型一起作为zset的底层数据结构。
其结构代码如下:

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;
robj *createZsetObject(void) {
    zset *zs = zmalloc(sizeof(*zs));// 初始化zset内存
    robj *o;
    zs->dict = dictCreate(&zsetDictType,NULL);//创建dict
    zs->zsl = zslCreate();// 创建skiplist
    o = createObject(OBJ_ZSET,zs);// 创建zset的robj
    o->encoding = OBJ_ENCODING_SKIPLIST;// 可看出,zset虽然时候dict+skiplist组成,但使用的编码是skipList类型
    return o;
}

上述的结构图如下:
在这里插入图片描述

当元素数量不多时,HT和SkipList的优势不明显,且更耗内存。因此Redis还会采用ZIpList结构作为zset的第二种底层结构。不过需要同时满足以下两个条件:

  • 元素数量<zset_max_ziplist_entries,默认值128

  • 每个元素都<zset_max_ziplist_value字节,默认值64

/** t_zset.c zaddGenericCommand */
// zadd添加元素时,现根据key查找zset对应的robj,没有则创建
zobj = lookupKeyWrite(c->db,key);
if (checkType(c,zobj,OBJ_ZSET)) goto cleanup;
if (zobj == NULL) {
    if (xx) goto reply_to_client; /* No key + XX option: nothing to do. */
    // 若不存在,且zset_max_ziplist_entries==0,则代表使用HT+SkipList
    // 若不存在,且zset_max_ziplist_value(字节大小)<value大小,则代表使用HT+SkipList
    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);
}

ZipList本身没有排序功能,而且没有键值对的概念,因此需要有zset通过逻辑编码实现相应功能。

  • ZipList是连续内存,因此score和element是紧挨在一起的两个entry,element在前,score在后

  • Score越小越接近队首,score越大越接近队尾,按照score值升序排列

内存结构图如下:
在这里插入图片描述

Hash数据类型

Hash数据类型的特点:——和zset数据类型相似

  1. 都是兼职存储

  2. 都需求根据键查询值

  3. 都需求键唯一

  4. 不同点:hash无序;zset要求有序

综上:Redis将使用类似Zset的底层结构作为hash的结构。Zset使用HT+SkipList / ZipList 底层数据结构,hash无需排序,所以无需使用SkipList,则最终为HT / ZipList底层数据结构。

  • Hash底层默认使用的ZipList编码,用以节省内存。ZipList中相邻的两个entry分别存储key和value

  • 当数据量较大时,hash结构会转为HT编码,也就是Dict。触发条件两个如下:

    • ZipList中元素超过hash-max-ziplist-entries(默认512)

    • ZipList中任一entry大小超过hash-max-ziplist-value(默认64byte)

ZipList编码 内存结构如下:

在这里插入图片描述

HT编码 内存结构如下:
在这里插入图片描述
通过hsetCommand # hashTypeLookupWriteOrCreate逻辑中,可看出hash 默认创建的是ZIPLIST编码。

void hsetCommand(client *c) {
    int i, created = 0;
    robj *o;
    /**
     * hmset key filed value ...
     * hmset user name jack age 10
     * argv[0] = hset
     * argv[1] = user
     * argv[2] = name
     * argv[3] = jack
     * ...
    */
    // 通过key查找对应的hash robj,若不存在则创建
    if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
    // 判断是否要转换类型,由ziplist -> ht
    hashTypeTryConversion(o,c->argv,2,c->argc-1);

    for (i = 2; i < c->argc; i += 2)
        //将file 和value 依次添加到dict
        created += !hashTypeSet(o,c->argv[i]->ptr,c->argv[i+1]->ptr,HASH_SET_COPY);
}
robj *hashTypeLookupWriteOrCreate(client *c, robj *key) {
    robj *o = lookupKeyWrite(c->db,key);
    if (checkType(c,o,OBJ_HASH)) return NULL;
    if (o == NULL) {
    	// 若robj为null,则创建
        o = createHashObject();
        dbAdd(c->db,key,o);
    }
    return o;
}
//默认创建的是ZIPLIST编码,那么插入逻辑中将会涉及到由ZIPLIST编码-> 转为 HT编码的过程
robj *createHashObject(void) {
    unsigned char *zl = ziplistNew();
    robj *o = createObject(OBJ_HASH, zl);
    o->encoding = OBJ_ENCODING_ZIPLIST;
    return o;
}

再看尝试转化HT编码的逻辑,通过下述代码可明显看出只判断的ZIPLIST的value大小,对于长度的判断并没有体现。

void hashTypeTryConversion(robj *o, robj **argv, int start, int end) {
    int i; size_t sum = 0;
    // 由于默认是ZIPLIST编码,若目前不是ZIPLIST编码,则代表已经是HT编码
    if (o->encoding != OBJ_ENCODING_ZIPLIST) return;
    for (i = start; i <= end; i++) {
        // 判断是否是key和value是不是sds,redis中数字和字符串都为sds
        if (!sdsEncodedObject(argv[i])) continue;
        size_t len = sdslen(argv[i]->ptr);
        // 判断key和value的字节长度是否>hash_max_ziplist_value(默认64byte)
        // 若大于,则需要转为HT编码
        if (len > server.hash_max_ziplist_value) {
            hashTypeConvert(o, OBJ_ENCODING_HT);
            return;
        }
        sum += len;
    }
    // 判断所有的key和value的总字节数,总字节数>1G时,则需要转为HT编码
    // Don't let ziplists grow over 1GB in any case
    if (!ziplistSafeToAdd(o->ptr, sum))  hashTypeConvert(o, OBJ_ENCODING_HT);
}

紧着看hashTypeSet方法,该方法是具体插入hash数据的方法。

int hashTypeSet(robj *o, sds field, sds value, int flags) {
    int update = 0;// 0:新增;1:更新
    // 判断是否是ZIPLIST编码
    if (o->encoding == OBJ_ENCODING_ZIPLIST) {
        unsigned char *zl, *fptr, *vptr;
        zl = o->ptr;
        // 若是ZIPLIST编码,则查找头节点
        fptr = ziplistIndex(zl, ZIPLIST_HEAD);
        // 若头节点为空,则直接新增
        if (fptr != NULL) {
            //若头节点不为空,通过field查找entry位置
            fptr = ziplistFind(zl, fptr, (unsigned char*)field, sdslen(field), 1);
            if (fptr != NULL) { // 若查询到对应的entry
                // 计算得出filed对应value的位置
                vptr = ziplistNext(zl, fptr);
                serverAssert(vptr != NULL);
                // 将update=1,代表更新
                update = 1;
                // 更新filed对应的value
                zl = ziplistReplace(zl, vptr, (unsigned char*)value,
                        sdslen(value));
            }
        }

        // update=0,代表新增,依次新增filed 和 value
        if (!update) {
            /* Push new field/value pair onto the tail of the ziplist */
            zl = ziplistPush(zl, (unsigned char*)field, sdslen(field),
                    ZIPLIST_TAIL);
            zl = ziplistPush(zl, (unsigned char*)value, sdslen(value),
                    ZIPLIST_TAIL);
        }
        o->ptr = zl;

        // 判断ziplist中entry的长度是否>hash_max_ziplist_entries(默认512)
        // 若大于,则需要转为HT编码
        if (hashTypeLength(o) > server.hash_max_ziplist_entries)
            hashTypeConvert(o, OBJ_ENCODING_HT);
    } else if (o->encoding == OBJ_ENCODING_HT) {
    	// ht编码直接插入或覆盖
        ...
    } else {
        serverPanic("Unknown hash encoding");
    }
    ...
}

最终发现,需转换编码逻辑中对长度的判断再具体插入的时候处理。仔细一想,就应该放在这个位置,因为hash是保证filed唯一,所以hset数据时可能时插入或覆盖,覆盖的话就没有长度的增加了。

若有缺失或不当描述部分,后续修改补充…

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值