Redis数据结构分析

143 阅读6分钟

简介

数据结构的基本操作不外乎增、删、改、查,Redis也不例外;它拥有五种常见的数据结构String(字符串),Hash(哈希),List(列表),Set(集合),Sorted Set(有序集合)数据结构,通过这些数据结构它能完成上述基本数据操作。同时它还有和HyperLogLog结构以及pub/sub (一种消息通信模式)模式。

在面试时询问Redis运行速度为什么这么快时,回答肯定会包含优秀的数据结构这一条,现在我就来学习Redis的数据结构设计优秀在哪里。

总览

Redis的五种常见数据结构只是对外的数据结构,通过指令:object encoding key,可以查询其内部编码(先设置k,v键值对)。

五种数据结构对应的内部编码如下:

见到其数据结构,那么就有两个问题:Redis如何选择其内部数据结构,以及这么做的好处在哪里。

String

下面就分析下String结构: 设置一个非常长的key(超过44个字节),发现其数据结构为(raw):

image.png

可以发现Redis的String数据结构内部编码有以下三种:int ,embstr ,raw。它们在何时会被使用呢?当使用如下命令后,Redis做了哪些操作呢,根据源码其大致分为以下几步

set key "this is a key which is greater than 39 bytes www.baidu.com"

  1. 解析SET命令,获取键和值,进行网络处理。
  2. 命令分发并执行
  3. 键值对象创建
  4. 数据库操作
  5. 持久化操作
  6. 返回响应给客户端。

主要看第三步的源码如下:

robj *createStringObject(const char *ptr, size_t len) {

    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)//默认为44

        return createEmbeddedStringObject(ptr,len);

    else

        return createRawStringObject(ptr,len);

}

可以发现key使用embstr和raw的默认限制是44,这个数字有点奇怪,现在看看Redis设计者为什么把这个值设置成44。看看两个创建对象的代码源码:

robj *createEmbeddedStringObject(const char *ptr, size_t len) {

//连续内存块通过zmalloc方法进行分配

    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->refcount1;

    o->lru0;



    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;

}

robj为Redis在内部定义为redisObject结构体,其代码如下:

struct redisObject {

    unsigned type:4;//对象类型

    unsigned encoding:4;//内部编码类型

    unsigned lru:LRU_BITS; //LRU计时时钟

    int refcount;//引用计数器

    void *ptr;//数据指针

};

type字段:表示当前对象使用的数据类型,Redis主要支持5种数据类型:string、hash、list、set、zset。可以使用type{key}命令查看对象所属类型,type命令返回的是值对象类型,键都是string类型。

encoding字段:表示Redis内部编码类型,encoding在Redis内部使用,代表当前对象内部采用哪种数据结构实现。理解Redis内部编码方式对于优化内存非常重要,同一个对象采用不同的编码实现内存占用存在明显差异。

lru字段:记录对象最后一次被访问的时间,当配置了maxmemory和maxmemory-policy=volatile-lru或者allkeys-lru时,用于辅助LRU算法删除键数据。可以使用object idletime{key}命令在不更新lru字段情况下查看当前键的空闲时间。

refcount字段:记录当前对象被引用的次数,用于通过引用次数回收内存,当refcount=0时,可以安全回收当前对象空间。使用object refcount{key}获取当前对象引用。当对象为整数且范围在[0-9999]时,Redis可以使用共享对象的方式来节省内存。

*ptr字段:与对象的数据内容相关,如果是整数,直接存储数据;否则表示指向数据的指针。

回到正文,

当其分配内存时可以发现,Redis通过zmalloc一次性分配内存,总大小为:sizeof(robj) + sizeof(sdshdr8) + len + 1

其中:

robj占16字节(4位类型+4位编码+24位LRU+32位引用计数+64位指针)6。

sdshdr8头占3字节(uint8_t len, uint8_t alloc, char flags)3。

len为字符串长度,+1用于存储\0结束符(C语言中表示字符串结束)。

内存布局:

robj *o指向内存块起始地址。

sdshdr8 *sh紧随robj之后(o+1)。

o->ptr指向sh+1,即SDS的buf数组起始位置,确保数据与元数据连续。

Redis默认使用jemalloc分配内存,以64字节为最小单位。扣除robj(16字节)、sdshdr8头(3字节)和\0(1字节),剩余空间为64 - 16 - 3 - 1 = 44字节。

再看看创建raw数据结构的源码:

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->refcount1;

    o->lru0;

    return o;

}
sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) {

    void *sh;

    sds s;

    char type = sdsReqType(initlen);

    /* Empty strings are usually created in order to append. Use type 8

     * since type 5 is not good at this. */

    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;

    int hdrlen = sdsHdrSize(type);

    unsigned char *fp; /* flags pointer. */

    size_t usable;



    assert(initlen + hdrlen + 1 > initlen); /* Catch size_t overflow */

    sh = trymalloc?

        s_trymalloc_usable(hdrlen+initlen+1, &usable) :

        s_malloc_usable(hdrlen+initlen+1, &usable);

    if (sh == NULL) return NULL;

    if (init==SDS_NOINIT)

        init = NULL;

    else if (!init)

        memset(sh, 0, hdrlen+initlen+1);

    s = (char*)sh+hdrlen;

    fp = ((unsigned char*)s)-1;

    usable = usable-hdrlen-1;

    if (usable > sdsTypeMaxSize(type))

        usable = sdsTypeMaxSize(type);

    switch(type) {

        case SDS_TYPE_5: {

            *fp = type | (initlen << SDS_TYPE_BITS);

            break;

        }

        case SDS_TYPE_8: {

            SDS_HDR_VAR(8,s);

            sh->len = initlen;

            sh->alloc = usable;

            *fp = type;

            break;

        }

        case SDS_TYPE_16: {

            SDS_HDR_VAR(16,s);

            sh->len = initlen;

            sh->alloc = usable;

            *fp = type;

            break;

        }

        case SDS_TYPE_32: {

            SDS_HDR_VAR(32,s);

            sh->len = initlen;

            sh->alloc = usable;

            *fp = type;

            break;

        }

        case SDS_TYPE_64: {

            SDS_HDR_VAR(64,s);

            sh->len = initlen;

            sh->alloc = usable;

            *fp = type;

            break;

        }

    }

    if (initlen && init)

        memcpy(s, init, initlen);

    s[initlen] = '\0';

    return s;

}

其代码需要创建两个部分:

调用sdsnewlen(ptr, len),根据输入字符串长度创建SDS结构体,返回SDS指针。

调用createObject,传入类型OBJ_STRING和SDS指针,生成redisObject。

其中SDS结构体独立分配内存,结构包含len(字符串长度)、alloc(分配空间)、flags(类型标志)和buf(字符数组);单独分配redisObject内存块,通过ptr字段指向SDS结构体的地址。

由此分析源码可以得出字符串数据结构的优劣点:

两种编码方式的优劣之处:

embstr:是连续内存适用高性能场景可以避免内存碎片,提升小字符串操作效率。

raw:支持动态修改(如APPEND操作),而embstr编码因内存连续无法原地修改;适合大字符串存储,避免预分配过多内存。

剩下的数据结构继续学习,如果有不同意见可以在我的公众号或者本文章下留言。

qrcode_for_gh_99433075514e_258.jpg