栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

redis底层数据结构分析

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

redis底层数据结构分析

redis 的数据存储结构 redis的string的底层数据结构 基本概念

redis的内部内部整体的数据结构就像是一个巨大的hashmap,内部的实现是数组实现
hash,如果发生冲突通过挂链去实现,然后每个dictEntry就是一个key/value对
象。dictEntry的key指向set key value命令中的key对应的对象,dictEntry的v
指向set key value命令中的value对应的对象。

在dictEntry内部包含了数据存储的key和v的值,同时包含一个dictEntry的next指
针连接落入同一个hash的对象。dictEntry的key和v的指针是指向的是
redisObject。
typedef struct dictEntry {
    
    // 键
    void *key;

    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;

    // 指向下个哈希表节点,形成链表
    struct dictEntry *next;

} dictEntry;
上面说到了redisObject,那么redisObject到底是什么呢?是redis server存储
最原子数据的数据结构。其中的void *ptr会指向真正的存储数据结构,我们set 
key value中的key和value其实由ptr指向真正保存的位置。
typedef struct redisObject {

    // 类型
    unsigned type:4;

    // 编码
    unsigned encoding:4;

    // 对象最后一次被访问的时间
    unsigned lru:REDIS_LRU_BITS; 

    // 引用计数
    int refcount;

    // 指向实际值的指针
    void *ptr;

} robj;
redis String类型转换
我们可能会认为redis在内部存储string都是用sds的数据结构实现的,其实在整个redis的数据存储过程中为了提高性能,内部做了很多优化。整体选择顺序应该是:

1. 整数,存储字符串长度小于21且能够转化为整数的字符串。

2. EmbeddedString,存储字符串长度小于39的字符串(REDIS_ENCODING_EMBSTR_SIZE_LIMIT)。

3. SDS,剩余情况使用sds进行存储。

embstr和sds的区别在于内存的申请和回收

1. embstr的创建只需分配一次内存,而raw为两次(一次为sds分配对象,另一次为
redisObject分配对象,embstr省去了第一次)。相对地,释放内存的次数也由两次变为一
次。

2. embstr的redisObject和sds放在一起,更好地利用缓存带来的优势

3. 缺点:redis并未提供任何修改embstr的方式,即embstr是只读的形式。对embstr的修改实际上是先转换为raw再进行修改。


String编码转换源码
 通过redis 内部的命令映射表我们找到set对应的处理函数为setCommand,相当于
 这个是处理set命令的入口函数,关注下tryObjectEncoding,内部对其实对
 Object进行转换。
void setCommand(redisClient *c) {
    int j;
    robj *expire = NULL;
    int unit = UNIT_SECONDS;
    int flags = REDIS_SET_NO_FLAGS;

    // 设置选项参数
    for (j = 3; j < c->argc; j++) {
        char *a = c->argv[j]->ptr;
        robj *next = (j == c->argc-1) ? NULL : c->argv[j+1];

        if ((a[0] == 'n' || a[0] == 'N') &&
            (a[1] == 'x' || a[1] == 'X') && a[2] == '') {
            flags |= REDIS_SET_NX;
        } else if ((a[0] == 'x' || a[0] == 'X') &&
                   (a[1] == 'x' || a[1] == 'X') && a[2] == '') {
            flags |= REDIS_SET_XX;
        } else if ((a[0] == 'e' || a[0] == 'E') &&
                   (a[1] == 'x' || a[1] == 'X') && a[2] == '' && next) {
            unit = UNIT_SECONDS;
            expire = next;
            j++;
        } else if ((a[0] == 'p' || a[0] == 'P') &&
                   (a[1] == 'x' || a[1] == 'X') && a[2] == '' && next) {
            unit = UNIT_MILLISECONDS;
            expire = next;
            j++;
        } else {
            addReply(c,shared.syntaxerr);
            return;
        }
    }

    // 尝试对值对象进行编码
    c->argv[2] = tryObjectEncoding(c->argv[2]);

    setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}

整个尝试编码转换的逻辑过程通过代码的注释应该是比较清楚了,过程如下:

只对长度小于或等于 21 字节,并且可以被解释为整数的字符串进行编码,使用整数存储尝试将 RAW 编码的字符串编码为 EMBSTR 编码,使用EMBSTR 编码这个对象没办法进行编码,尝试从 SDS 中移除所有空余空间,使用SDS编码

// 尝试对字符串对象进行编码,以节约内存。
robj *tryObjectEncoding(robj *o) {
    long value;

    sds s = o->ptr;
    size_t len;
    redisAssertWithInfo(NULL,o,o->type == REDIS_STRING);

    // 只在字符串的编码为 RAW 或者 EMBSTR 时尝试进行编码
    if (!sdsEncodedObject(o)) return o;

     // 不对共享对象进行编码
     if (o->refcount > 1) return o;

    // 对字符串进行检查
    // 只对长度小于或等于 21 字节,并且可以被解释为整数的字符串进行编码
    len = sdslen(s);
    if (len <= 21 && string2l(s,len,&value)) {
        if (server.maxmemory == 0 &&
            value >= 0 &&
            value < REDIS_SHARED_INTEGERS)
        {
            decrRefCount(o);
            incrRefCount(shared.integers[value]);
            return shared.integers[value];
        } else {
            if (o->encoding == REDIS_ENCODING_RAW) sdsfree(o->ptr);
            o->encoding = REDIS_ENCODING_INT;
            o->ptr = (void*) value;
            return o;
        }
    }

    // 尝试将 RAW 编码的字符串编码为 EMBSTR 编码
    if (len <= REDIS_ENCODING_EMBSTR_SIZE_LIMIT) {
        robj *emb;

        if (o->encoding == REDIS_ENCODING_EMBSTR) return o;
        emb = createEmbeddedStringObject(s,sdslen(s));
        decrRefCount(o);
        return emb;
    }

    // 这个对象没办法进行编码,尝试从 SDS 中移除所有空余空间
    if (o->encoding == REDIS_ENCODING_RAW &&
        sdsavail(s) > len/10)
    {
        o->ptr = sdsRemoveFreeSpace(o->ptr);
    }

    
    return o;
}
redis sds的介绍
在C语言中,字符串可以用''结尾的char数组标示。这种简单的字符串表示,在大
多数情况下都能满足要求,但是不能高效的计算length和append数据。所以Redis自
己实现了SDS(简单动态字符串)的抽象类型。

 SDS的数据结构如下,len表示sdshdr中数据的长度,free表示sdshdr中剩余的空
 间,buf表示实际存储数据的空间。
 
 sdslen的函数有一个细节需要我们注意,那就是通过(s-(sizeof(struct 
 sdshdr)))来计算偏移量,之所以需要这么计算是因为sds的指针指向的是char 
 buf[]位置,所以我们需要访问sdshdr的首地址的时候需要减去偏移量。

struct sdshdr {
    
    // buf 中已占用空间的长度
    int len;

    // buf 中剩余可用空间的长度
    int free;

    // 数据空间
    char buf[];
};


static inline size_t sdslen(const sds s) {
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
    return sh->len;
}


static inline size_t sdsavail(const sds s) {
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
    return sh->free;
}
redis list底层数据结构 基础介绍
redis list数据结构底层采用压缩列表ziplist或linkedlist两种数据结构进行存
储,首先以ziplist进行存储,在不满足ziplist的存储要求后转换为linkedlist列
表。
 当列表对象同时满足以下两个条件时,列表对象使用ziplist进行存储,否则用
 linkedlist存储。

列表对象保存的所有字符串元素的长度小于64字节列表对象保存的元素数量小于512个。 list元素添加过程

 list的数据添加根据传入的变量个数一个个顺序添加,整个顺序如下:
 * 创建list对象并添加到db的数据结构当中
 * 针对每个待插入的元素添加到list当中
void pushGenericCommand(redisClient *c, int where) {

    int j, waiting = 0, pushed = 0;

    // 取出列表对象
    robj *lobj = lookupKeyWrite(c->db,c->argv[1]);

    // 如果列表对象不存在,那么可能有客户端在等待这个键的出现
    int may_have_waiting_clients = (lobj == NULL);

    if (lobj && lobj->type != REDIS_LIST) {
        addReply(c,shared.wrongtypeerr);
        return;
    }

    // 将列表状态设置为就绪
    if (may_have_waiting_clients) signalListAsReady(c,c->argv[1]);

    // 遍历所有输入值,并将它们添加到列表中
    for (j = 2; j < c->argc; j++) {

        // 编码值
        c->argv[j] = tryObjectEncoding(c->argv[j]);

        // 如果列表对象不存在,那么创建一个,并关联到数据库
        if (!lobj) {
            lobj = createZiplistObject();
            dbAdd(c->db,c->argv[1],lobj);
        }

        // 将值推入到列表
        listTypePush(lobj,c->argv[j],where);

        pushed++;
    }

    // 返回添加的节点数量
    addReplyLongLong(c, waiting + (lobj ? listTypeLength(lobj) : 0));

    // 如果至少有一个元素被成功推入,那么执行以下代码
    if (pushed) {
        char *event = (where == REDIS_HEAD) ? "lpush" : "rpush";

        // 发送键修改信号
        signalModifiedKey(c->db,c->argv[1]);

        // 发送事件通知
        notifyKeyspaceEvent(REDIS_NOTIFY_LIST,event,c->argv[1],c->db->id);
    }

    server.dirty += pushed;
}

list的每个元素的插入过程中,我们会对是否需要进行转码作两个判断:

对每个插入元素的长度进行判断是否进行ziplist->linkedlist的转码。对list总长度是否超过ziplist最大长度的判断。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/711624.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号