掘金 后端 ( ) • 2022-05-27 19:49

highlight: atom-one-dark theme: fancy

前言

通过前面的学习我们了解了Redis6.0中典型的数据结构实现, 我们都知道Redis常用的数据类型有五种, 分别为String, Set, Zset, Hash, List。这五种类型的的底层都使用了那些编码结构来实现呢?在前期的【Redis源码系列】Redis6.0 DB结构以及渐进式rehash超详细源码解读中我们分析了redisDB结构的dictRntry中存储了redisObject字段, 本期我们来详细分析redisObject对象的编码结构。

前期回顾 【Redis源码系列】Redis6.0数据结构详解--ziplist篇 【Redis源码系列】Redis6.0数据结构详解--skiplist篇 【Redis源码系列】Redis6.0数据结构详解--sds篇

解析redisObject

redisObject结构体定义在src/server.h下, 具体结构图下:

image.png

  • type字段: 表示当前对象存储的数据类型, 即我们常见的String, Hash等, 具体的枚举值如下: Define | value | 含义 | | ---------- | - | ----------- | | OBJ_STRING | 0 | 字符串 | | OBJ_LIST | 1 | 列表 | | OBJ_SET | 2 | 集合 | | OBJ_ZSET | 3 | 有序集 | | OBJ_HASH | 4 | 哈希表 | | OBJ_MODULE | 5 | 模块(R4.0后可以通过外部模块对Redis进行功能性扩展) | | OBJ_STREAM | 6 | 流(R5.0新增用于消息队列)

  • encoding字段: 用于标识当前数据结构的编码方式, 具体枚举值如下: Defaine | value
    | ----------------------- | -- | | OBJ_ENCODING_RAW | 0 | | OBJ_ENCODING_INT | 1 | | OBJ_ENCODING_HT | 2 | | OBJ_ENCODING_ZIPMAP | 3 | | OBJ_ENCODING_LINKEDLIST | 4 | | OBJ_ENCODING_ZIPLIST | 5 | | OBJ_ENCODING_INTSET | 6 | | OBJ_ENCODING_SKIPLIST | 7 | | OBJ_ENCODING_EMBSTR | 8 | | OBJ_ENCODING_QUICKLIST | 9 | | OBJ_ENCODING_STREAM | 10 |

  • lru字段: 当淘汰策略是lru时记录相对于lru_clock全局时钟的操作时间, 当lfu时记录操作频率。

解析encoding编码

字符串(String)

字符串使用三种编码方式来存储OBJ_ENCODING_RAW, OBJ_ENCODING_INT,OBJ_ENCODING_EMBSTR, 当我们执行set命令时, 首先调用src/t_string.csetCommand方法, 进行参数解析, 并且创建redis对象, 逻辑如下:

void setCommand(client *c) {
    int j;
    robj *expire = NULL;
    int unit = UNIT_SECONDS;
    int flags = OBJ_SET_NO_FLAGS;

    for (j = 3; j < c->argc; j++) {
        /*** 省略参数解析 ***/
    }
    
    // 创建redisObject对象
    c->argv[2] = tryObjectEncoding(c->argv[2]);
    setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}

在创建redisObject时会调用tryObjectEncoding方法, 设置不同的对象编码格式, 逻辑如下:

robj *tryObjectEncoding(robj *o) {
    long value;
    sds s = o->ptr;
    size_t len;

    /* 断言确保数据类型时字符串 */
    serverAssertWithInfo(NULL,o,o->type == OBJ_STRING);

    if (!sdsEncodedObject(o)) return o;

    /* 如果当前对象已被引用, 证明已经设置完成 */
     if (o->refcount > 1) return o;

    /* 确认当前字符串是否可以被设置为长整数编码, 当字符串长度超过20时, 不能被表示为int32 or int64 编码, string2l表示当前字符串可以转换为整形 */
    len = sdslen(s);
    if (len <= 20 && string2l(s,len,&value)) {
        /* 当服务没有设置最大内存限制或者maxmemory-policy设置的淘汰算法中不计算LRU值时, 0 到 10000 之间OBJ_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 {
            // 如果是 RAW 或 EMBSTR 编码, 则转换为 INT编码
            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);
                return createStringObjectFromLongLongForValue(value);
            }
        }
    }

    /* 如果长度小于44则尝试将编码转换为 EMB  */
    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;
    }
    
    // 不能在进行编码, 尝试清除多余的空间
    trimStringObjectIfNeeded(o);

    return o;
}

总结如下:

  • 如果值可以转换为整形, 则优先使用OBJ_ENCODING_INT进行编码
  • 如果值的长度小于44, 则转换为OBJ_ENCODING_EMBSTR编码
  • 默认使用OBJ_ENCODING_RAW进行编码

列表(List)

Redis6.0列表使用OBJ_ENCODING_QUICKLIST编码, 不同于3.0版本使用OBJ_ENCODING_ZIPLIST,OBJ_ENCODING_QUICKLIST两种编码方式来进行存储, 这是需要大家注意的一点。 在6.0.0版本的lPush指令中只会调用createQuicklistObject创建QuickList编码的对象:

// Redis6.0.0版本
void pushGenericCommand(client *c, int where) {
    /**** 省略 ****/
    for (j = 2; j < c->argc; j++) {
        if (!lobj) {
            // 创建Quicklist对象
            lobj = createQuicklistObject();
            quicklistSetOptions(lobj->ptr, server.list_max_ziplist_size,
                                server.list_compress_depth);
            dbAdd(c->db,c->argv[1],lobj);
        }
        listTypePush(lobj,c->argv[j],where);
        pushed++;
    }
   /**** 省略 ****/
}

// Redis3.0.0版本
void pushGenericCommand(redisClient *c, int where) {
    /**** 省略 ****/
    for (j = 2; j < c->argc; j++) {
        c->argv[j] = tryObjectEncoding(c->argv[j]);
        if (!lobj) {
            // 创建ziplist对象
            lobj = createZiplistObject();
            dbAdd(c->db,c->argv[1],lobj);
        }
        listTypePush(lobj,c->argv[j],where);
        pushed++;
    }
   /**** 省略 ****/
}

同时经过仔细的分析, listTypeConvert方法的enc参数(编码方式), 也只有一种, 并且createZiplistObject方法已经不再被调用。

image.png image.png

集合(Set)

集合使用REDIS_ENCODING_INTSET, REDIS_ENCODING_HT两种编码方式来存储, 在执行SADD命令时, 会首先检查元素是否可以用整数标识, 如果可以, 则使用REDIS_ENCODING_INTSET编码, 否则使用REDIS_ENCODING_HT编码创建对象。

// sadd命令入口函数
void saddCommand(client *c) {
    robj *set;
    int j, added = 0;

    set = lookupKeyWrite(c->db,c->argv[1]);
    if (set == NULL) {
        // 创建集合对象
        set = setTypeCreate(c->argv[2]->ptr);
        dbAdd(c->db,c->argv[1],set);
    }
    /*** 省略 ***/
}
// 创建集合对象方法:setTypeCreate 
// 当对象具有整数可编码值时,将返回intset。否则,使用一个常规的哈希表。
robj *setTypeCreate(sds value) {
    if (isSdsRepresentableAsLongLong(value,NULL) == C_OK)
        return createIntsetObject();
    return createSetObject();
}
// 创建一个普通hash编码的集合对象
robj *createSetObject(void) {
    dict *d = dictCreate(&setDictType,NULL);
    robj *o = createObject(OBJ_SET,d);
    o->encoding = OBJ_ENCODING_HT;
    return o;
}
// 创建一个整形编码的集合对象
robj *createIntsetObject(void) {
    intset *is = intsetNew();
    robj *o = createObject(OBJ_SET,is);
    o->encoding = OBJ_ENCODING_INTSET;
    return o;
}

有序集合(Zset)

有序集合使用OBJ_ENCODING_SKIPLIST,OBJ_ENCODING_ZIPLIST两种编码结构来保存对象, 通过两项服务配置来控制转换规则:

//仅当排序集的长度和元素低于以下限制时,才使用此编码, 后面数字为默认值, 可在redis.conf中配置
zset-max-ziplist-entries 128 
zset-max-ziplist-value 64 

当我们执行zadd添加一个有序几何元素时, 会根据上面配置的条件做判断创建何种类型的对象:

void zaddGenericCommand(client *c, int flags) {
    /*** 省略 ***/
    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))
        {
            // 创建skiplist编码对象
            zobj = createZsetObject();
        } else {
            // 创建ziplist编码对象
            zobj = createZsetZiplistObject();
        }
        dbAdd(c->db,key,zobj);
    }     
    /*** 省略 ***/

哈希(Hash)

哈希使用REDIS_ENCODING_ZIPLIST, OBJ_ENCODING_HT两种编码方式存储, 与有序集合类似, 通过redis.conf配置文件中的参数来控制转换规则, 具体配置如下:

hash-max-ziplist-entries 512

当执行hset命令时, 逻辑如下:

// 执行hset命令逻辑
void hsetCommand(client *c) {
    int i, created = 0;
    robj *o;
    /*** 省略 ***/ 
    // 创建对象, 使用 ziplist编码
    if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
    hashTypeTryConversion(o,c->argv,2,c->argc-1);
    
    // 循环设置hash参数
    for (i = 2; i < c->argc; i += 2)
        created += !hashTypeSet(o,c->argv[i]->ptr,c->argv[i+1]->ptr,HASH_SET_COPY);
    
    /*** 省略 ***/ 
}

// 设置元素类型
int hashTypeSet(robj *o, sds field, sds value, int flags) {
    int update = 0;
    
    if (o->encoding == OBJ_ENCODING_ZIPLIST) {
        /*** 省略 ***/
        // 如果当前编码类型是 OBJ_ENCODING_ZIPLIST, 并且当前对象元素数量 > hash_max_ziplist_entries 配置, 则转换为 OBJ_ENCODING_HT 编码
        if (hashTypeLength(o) > server.hash_max_ziplist_entries)
            hashTypeConvert(o, OBJ_ENCODING_HT);
    } else if (o->encoding == OBJ_ENCODING_HT) {
        /*** 省略 ***/
    } else {
        serverPanic("Unknown hash encoding");
    }
    /*** 省略 ***/
    return update;
}

总结

本文我们分了Redis6.0中的各种数据类型对应的编码格式, 基于源码分析, 并未过多深入讨论这样设计的原因以及优缺点。其中需要注意的是List编码方式和老版本实现是不一样的, 目前能够查看到的很多资料都未描述到这一点。本文总结如下:

数据类型 编码方式 转换规则 REDIS_STRING REDIS_ENCODING_INT 如果长度小于20并且可以value可以转换为整形 REDIS_ENCODING_EMBSTR 如果长度小于44, 使用此编码 REDIS_ENCODING_RAW 默认编码 REDIS_LIST OBJ_ENCODING_QUICKLIST 默认编码, 6.0.0版本无其他编码 REDIS_HASH REDIS_ENCODING_ZIPLIST 初始化默认编码 REDIS_ENCODING_HT 当元素超过配置: hash-max-ziplist-entries时使用此编码, 默认值是512 REDIS_SET REDIS_ENCODING_INTSET 如果member可以转换为整形, 则使用REDIS_ENCODING_INTSET编码, 否则使用REDIS_ENCODING_HT编码 REDIS_ENCODING_HT REDIS_ZSET REDIS_ENCODING_ZIPLIST 如果 zset-max-ziplist-entries 配置为0并且, 元素长度小于 zset-max-ziplist-value 配置(默认64), 则使用REDIS_ENCODING_SKIPLIST编码, 否则使用REDIS_ENCODING_ZIPLIST编码 REDIS_ENCODING_SKIPLIST

觉得对大家理解有帮助帮忙点个赞吧 :)