Redis 对象处理机制 redisObject

在Redis的命令中,用于对键(key)进行处理的命令占了很大一部分, 而对于键所保存的值的类型(后简称”键的类型”),键能执行的命令又各不相同。

比如 string list hash set zset 怎么处理key的过期时间?

这就涉及到Redis的对象系统了。

(1) Redis对象系统是什么

Redis 的 key 一般是 String 类型,但 value 可以是很多类型(String/List/Hash/Set/ZSet等),所以 Redis 要想存储多种数据类型,就要设计一个通用的对象进行封装,这个对象就是 redisObject。

redisObject、数据类型、数据结构之间的关系

redisObject 是Redis类型系统的核心, 数据库中的每个键、值,以及Redis本身处理的参数,都表示为这种数据类型。

为了应对不同的对象类型(type)和对象编码(encoding),Redis构建了自己的类型系统,这个系统的主要功能包括:

  1. redisObject 对象。
  2. 基于 redisObject 对象的类型检查。
  3. 基于 redisObject 对象的显式多态函数。
  4. 对 redisObject 进行分配、共享和销毁的机制。

(1.1) redisObject结构

Redis 使用的基本数据对象结构体 redisObject

redisObject

(1.2) redisObject的作用

redisObject 的作用在于:

  1. 为多种数据类型提供统一的表示方式
  2. 同一种数据类型,底层可以对应不同实现,节省内存
  3. 支持对象共享和引用计数,共享对象存储一份,可多次使用,节省内存

redisObject 解耦了 上层数据类型 和 底层数据结构 ,使研发可以更关注数据类型的使用。


(2) redisObject原理

实际上,Redis每个键都带有类型信息、编码方式(数据结构) 等。

redisObject

另外,Redis 的每一种数据类型,比如字符串、列表、有序集, 它们都拥有不只一种底层实现(Redis内部称之为编码,encoding),这说明,每当对某种数据类型的键进行操作时,程序都必须根据键所采取的编码,进行不同的操作。

(2.1) redisObject里type对应的Redis对象

type对应redisObject的数据类型,对应redis里的string list set sorted set hash stream 等。

redisObject

// file: src/server.h

/* 
 * 实际的Redis对象 
 * The actual Redis Object 
 */
#define OBJ_STRING 0    /* String 对象 */
#define OBJ_LIST 1      /* List 对象 */
#define OBJ_SET 2       /* Set 对象 */
#define OBJ_ZSET 3      /* Sorted set 对象 */
#define OBJ_HASH 4      /* Hash 对象 */

#define OBJ_MODULE 5    /* Module 对象 */
#define OBJ_STREAM 6    /* Stream 对象 */

(2.2) redisObject里encoding对应的对象编码

encoding对应redis里的编码方式,有 raw int ht zipmap linkedlist ziplist intset skiplist embstr quicklist stream

redisObjectRedis对象(Redis Type) 以及 对象编码(Objects encoding) 三者之间的关系:

redisObject、数据类型、数据结构之间的关系

// file: src/server.h

/* 
 * 对象编码。 
 * 某些类型的对象(如字符串和哈希)可以在内部以多种方式表示。 
 * 对象的"encoding"字段设置为此对象的此字段之一。
 */
#define OBJ_ENCODING_RAW 0     /* Raw representation */
#define OBJ_ENCODING_INT 1     /* Encoded as integer */
#define OBJ_ENCODING_HT 2      /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3  /* Encoded as 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 */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */

(3) redisObject源码解析

源码: https://github.com/redis/redis/blob/6.0/src/server.h#L633

(3.1) redisObject结构定义

redisObject

// file: src/server.h 

/*
 * redis对象
 */
typedef struct redisObject {
    unsigned type:4;  // 数据类型 (string/list/hash/set/zset等)
    unsigned encoding:4;  // 编码方式 
    unsigned lru:LRU_BITS;  // LRU时间(相对于全局 lru_clock) 
                            // 或 LFU数据(低8位保存频率 和 高16位保存访问时间)。  
                            // LRU_BITS为24个bits
    int refcount;  // 引用计数  4字节
    void *ptr;  // 指针 指向对象的值  8字节
} robj;

type占用内存空间 4bits
encoding占用内存空间 4bits
lru占用内存空间24bits
refcount占用内存空间4字节
*ptr占用内存空间8字节
redisObject总共占用内存空间 16字节


(4) redisObject、data-type、date-structure之间的关系

redis 里常用的对象类型有 string、list、hash、set、zset

(4.1) 创建string类型对象

创建字符串对象有3种方法,创建整数对象、创建嵌入字符串对象 和 原始(raw)字符串对象

(4.1.1) 创建嵌入式字符串-emb_str

RedisObject-SDS-EMB编码字符串-内存结构

/* 
 * 如果小于 OBJ_ENCODING_EMBSTR_SIZE_LIMIT,则使用 EMBSTR 编码创建字符串对象,否则使用 RAW 编码。
 
 * 选择当前限制 44,以便我们分配为 EMBSTR 的最大字符串对象仍然适合 jemalloc 的 64 字节区域。
*/
#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); // 编码 emb_str
    else
        return createRawStringObject(ptr,len);  // 编码 raw
}
// file: src/object.c

/* 
 * 创建一个编码为 OBJ_ENCODING_EMBSTR 的字符串对象,
 * 这是一个对象,其中 sds 字符串实际上是一个不可修改的字符串,分配在与对象本身相同的块中。
 *  
 * @param *ptr
 * @param len
 */
robj *createEmbeddedStringObject(const char *ptr, size_t len) {
    // 分配内存空间 
    // 包括 redisObject结构体空间、sdshdr8结构体空间、字符串长度、以及结束符"\0"的长度1 
    // robj长度是16字节  sdshdr8长度是3字节 
    robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);
    
    // o是redisObject结构体的变量,o+1 表示将内存地址从变量o开始往后移动1,这个位置是sdshdr8结构体内存地址开始的地方。
    struct sdshdr8 *sh = (void*)(o+1);

    // redisObject 类型为string,编码为embstr 
    o->type = OBJ_STRING;
    // 编码 设置为 OBJ_ENCODING_EMBSTR
    o->encoding = OBJ_ENCODING_EMBSTR;
    // 把 redisObject 中的指针 ptr,指向 SDS 结构中的字符数组
    // sh+1 表示将内存地址从变量sh开始往后移动1,这个位置是字符串内存地址开始的地方。
    o->ptr = sh+1;
    // 引用计数设置为1 
    o->refcount = 1;
    // 设置内存淘汰策略
    if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
        o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
    } else {
        o->lru = LRU_CLOCK();
    }

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

使用一块连续的内存空间,来同时保存 redisObjectSDS 结构。这样一来,内存分配只有一次,而且也避免了内存碎片。

(4.1.2) 创建原始字符串-raw

RedisObject-SDS-RAW编码字符串-内存结构

// file: src/object.c 

/*
 * 使用编码OBJ_ENCODING_RAW创建一个字符串对象,
 * 这是一个纯字符串对象,其中 o->ptr 指向正确的 sds 字符串。
 */
robj *createRawStringObject(const char *ptr, size_t len) {
    // redisObject对象  类型为string,编码为raw  
    return createObject(OBJ_STRING, sdsnewlen(ptr,len));
}

对应的创建字符串的方法

// file: src/object.c

/*
 * 创建一个redisObject对象
 *
 * @param type redisObject的类型
 * @param *ptr 值的指针
 */
robj *createObject(int type, void *ptr) {
    // 为redisObject结构体分配内存空间
    robj *o = zmalloc(sizeof(*o));
    //设置redisObject的类型
    o->type = type;
    // 设置默认encoding为raw,后面可能会更改 
    o->encoding = OBJ_ENCODING_RAW;
    // 直接将传入的指针赋值给redisObject中的指针。 指向 char[]
    o->ptr = ptr;
    // 引用计数设置成1 
    o->refcount = 1;

    // 将lru字段设置为当前的 lruclock(分钟分辨率),或者 LFU 计数器。 
    // 判断内存过期策略
    if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
        // 对应lfu 
        // LFU_INIT_VAL=5 对应二进制是 0101 
        // 或运算 
        o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
    } else {
        // 对应lru 
        o->lru = LRU_CLOCK();
    }
    return o;
}

(4.1.3) 创建整数型字符串

在这个是在redis启动时,main方法里调用 创建的 createSharedObjects() 创建的

/* 
 * createStringObjectFromLongLongWithOptions() 的包装器总是要求尽可能创建一个共享对象。
 */
robj *createStringObjectFromLongLong(long long value) {
    return createStringObjectFromLongLongWithOptions(value,0);
}
// file: src/object.c

/* 
 * 从 long long value创建一个字符串对象。 如果可能,返回一个共享整数对象,或者至少是一个整数编码对象。
 *
 * 如果 valueobj 不为零,该函数将避免返回共享整数,因为该对象将用作Redis键空间中的值
 * (例如,当使用INCR命令时),因此我们希望 每一个key的 LFU/LRU 值都是特别的。
 *
 * 在 src/slowlog.c module.c文件里用到了
 */
robj *createStringObjectFromLongLongWithOptions(long long value, int valueobj) {
    robj *o;

    if (server.maxmemory == 0 ||
        !(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS))
    {
        // 如果 maxmemory 策略允许,即使 valueobj 为真,我们仍然可以返回共享整数。 
        valueobj = 0;
    }

    // 默认 0 <= value < 10000 并且  valueobj == 0 时,使用享元模式,节省内存
    if (value >= 0 && value < OBJ_SHARED_INTEGERS && valueobj == 0) {
        // 引用次数+1
        incrRefCount(shared.integers[value]);
        // redisObject 赋值
        o = shared.integers[value];
    } else {
        if (value >= LONG_MIN && value <= LONG_MAX) {
            // 创建redisObject,type为string,编码为int
            o = createObject(OBJ_STRING, NULL);
            o->encoding = OBJ_ENCODING_INT;
            o->ptr = (void*)((long)value);
        } else {
            // 创建redisObject,type为string,编码为raw
            o = createObject(OBJ_STRING,sdsfromlonglong(value));
        }
    }
    return o;
}

(4.1.4) 复制字符串对象-dupStringObject

/* 
 * 复制一个字符串对象,并保证返回的对象与原始对象具有相同的编码。
 *
 * 此函数还保证复制一个小整数对象(或包含一个小整数表示的字符串对象)将始终产生一个未共享的新对象(refcount == 1)。
 *
 * 生成的对象始终将引用次数设置为 1。
 */
robj *dupStringObject(const robj *o) {
    robj *d;

    serverAssert(o->type == OBJ_STRING);

    // 判断string类型的编码类型
    switch(o->encoding) {
    case OBJ_ENCODING_RAW:  // 原始字符串
        return createRawStringObject(o->ptr,sdslen(o->ptr));
    case OBJ_ENCODING_EMBSTR:  // 嵌入式字符串
        return createEmbeddedStringObject(o->ptr,sdslen(o->ptr));
    case OBJ_ENCODING_INT:  // 整数
        d = createObject(OBJ_STRING, NULL);
        d->encoding = OBJ_ENCODING_INT;
        d->ptr = o->ptr;
        return d;
    default:
        serverPanic("Wrong encoding.");
        break;
    }
}

(4.2) 创建列表对象-list

创建列表对象有2种方法,创建快速列表对象 和 创建压缩列表对象
创建快速列表对象 createQuicklistObject
创建压缩列表对象 createZiplistObject

(4.2.1) 创建快速列表对象-createQuicklistObject

/*
 * 创建快速列表对象
 * 
 * 在 src/t_list.c 文件里用到了 lobj = createQuicklistObject(); 
 */
robj *createQuicklistObject(void) {
    // 创建快速列表
    quicklist *l = quicklistCreate();
    // 创建redisObject 类型是list,编码是quicklist 
    robj *o = createObject(OBJ_LIST,l);
    o->encoding = OBJ_ENCODING_QUICKLIST;
    return o;
}

(4.2.2) 创建压缩列表对象-createZiplistObject

/*
 *
 */
robj *createZiplistObject(void) {
    // 创建压缩列表
    unsigned char *zl = ziplistNew();
    // 创建redisObject 类型是list,编码是ziplist 
    robj *o = createObject(OBJ_LIST,zl);
    o->encoding = OBJ_ENCODING_ZIPLIST;
    return o;
}

(4.3) 创建哈希对象-hash

robj *createHashObject(void) {
    unsigned char *zl = ziplistNew();
    // 创建redisObject,类型hash,编码方式ziplist(压缩列表)
    robj *o = createObject(OBJ_HASH, zl);
    o->encoding = OBJ_ENCODING_ZIPLIST;
    return o;
}

(4.4) 集合

集合有两种编码 整数 和 哈希表

(4.4.1) 创建整数集合-createIntsetObject

/*
 * 创建整数集合对象
 * 
 * 在 t_set.c、rdb.c 用到了
 */
robj *createIntsetObject(void) {
    // 创建整数集合
    intset *is = intsetNew();
    // 创建redisObject  类型set,编码方式intset(整数集合)
    robj *o = createObject(OBJ_SET,is);
    o->encoding = OBJ_ENCODING_INTSET;
    return o;
}

(4.4.2) 创建集合-createSetObject

/*
 * 创建set对象
 * 
 * 在 t_set.c、rdb.c 用到了
 */
robj *createSetObject(void) {
    // 创建哈希表
    dict *d = dictCreate(&setDictType,NULL);
    // 创建redisObject  类型set,编码方式ht(哈希表)
    robj *o = createObject(OBJ_SET,d);
    o->encoding = OBJ_ENCODING_HT;
    return o;
}

(4.5) 有序集合

(4.5.1) 创建编码为压缩列表的有序集合对象-createZsetZiplistObject

robj *createZsetZiplistObject(void) {
    // 创建压缩列表
    unsigned char *zl = ziplistNew();
    // 创建redisObject, 类型zset,编码方式ziplist(压缩列表)
    robj *o = createObject(OBJ_ZSET,zl);
    o->encoding = OBJ_ENCODING_ZIPLIST;
    return o;
}

(4.5.2) 创建有序集合对象-createZsetObject

robj *createZsetObject(void) {
    // 分配内存
    zset *zs = zmalloc(sizeof(*zs));
    robj *o;
    // 创建哈希表
    zs->dict = dictCreate(&zsetDictType,NULL);
    // 创建跳表
    zs->zsl = zslCreate();
    // 创建redisObject, 类型zset,编码方式skiplist(跳表)
    o = createObject(OBJ_ZSET,zs);
    o->encoding = OBJ_ENCODING_SKIPLIST;
    return o;
}

(4.6) 模块

robj *createModuleObject(moduleType *mt, void *value) {
    // 
    moduleValue *mv = zmalloc(sizeof(*mv));
    mv->type = mt;
    mv->value = value;
    // 创建redisObject
    return createObject(OBJ_MODULE,mv);
}

(4.7) 流

robj *createStreamObject(void) {
    stream *s = streamNew();
    // 创建redisObject,类型stream,编码方式stream
    robj *o = createObject(OBJ_STREAM,s);
    o->encoding = OBJ_ENCODING_STREAM;
    return o;
}
/* Set a special refcount in the object to make it "shared":
 * incrRefCount and decrRefCount() will test for this special refcount
 * and will not touch the object. This way it is free to access shared
 * objects such as small integers from different threads without any
 * mutex.
 *
 * A common patter to create shared objects:
 *
 * robj *myobject = makeObjectShared(createObject(...));
 *
 */
robj *makeObjectShared(robj *o) {
    serverAssert(o->refcount == 1);
    o->refcount = OBJ_SHARED_REFCOUNT;
    return o;
}

(5) redisObject里内存优化

// file: server.h 

/*
 * redis对象
 */
typedef struct redisObject {
    unsigned type:4;  // 数据类型  4个bits
    unsigned encoding:4;  // 编码方式 4个bits
    unsigned lru:LRU_BITS;  // LRU时间(相对于全局 lru_clock) 
                            // 或 LFU数据(低8位保存频率 和 高16位保存访问时间)。  
                            // LRU_BITS为24个bits
    int refcount;  // 引用计数  4字节
    void *ptr;  // 指针 指向对象的值  8字节
} robj;

(5.1) 位域定义方法

typeencodinglru 三个变量后面都有一个冒号,并紧跟着一个数值,表示该元数据占用的比特数。
其中,typeencoding 分别占 4bitslru 占用 24bits (LRU_BITS = 24bits) ,三个字段一共占用32bits=4字节

变量后使用冒号和数值的定义方法。是C语言中的位域定义方法,可以用来有效地节省内存开销。

当一个变量占用不了一个数据类型的所有 bits 时,就可以使用位域定义方法,把一个数据类型中的 bits,划分成多个位域,每个位域占一定的 bit 数。这样一来,一个数据类型的所有 bits 就可以定义多个变量了,从而也就有效节省了内存开销。

(5.2) 嵌入式字符串

SDS 在保存比较小的字符串时,会使用嵌入式字符串的设计方法,将字符串直接保存在 redisObject 结构体中。然后在 redisObject 结构体中,存在一个指向值的指针 ptr,而一般来说,这个 ptr 指针会指向值的数据结构。

以创建一个 String 类型的值为例,Redis 会调用 createStringObject 函数,来创建相应的 redisObject,而这个 redisObject 中的 ptr 指针,就会指向 SDS 数据结构,如下图所示。

// file: object.c

/* 
 * 如果<=44,EMBSTR 编码创建一个字符串对象,否则使用 RAW 编码。
 *
 * The current limit of 44 is chosen so that the biggest string object
 * we allocate as EMBSTR will still fit into the 64 byte arena of jemalloc. 
 */
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
robj *createStringObject(const char *ptr, size_t len) {
    // 判断字符长度, <=44 使用 EMBSTR,否则使用 RAW 
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
        return createEmbeddedStringObject(ptr,len); // EMBSTR编码
    else
        return createRawStringObject(ptr,len); // RAW编码
}

String对象类型,存储的字符串长度<=44时,用embstr(嵌入式字符串,redisObject和SDS分配的内存是连起来的);字符串长度>44时,使用raw格式存储;存储的的是一个「数字」时,会使用long long类型来存储,节省内存。

同理,hash / set / zset 在数据量少时,采用 压缩列表(ziplist) 存储,否则就转为 哈希表(dictht) 来存。

嵌入字符串

使用一块连续的内存空间,来同时保存 redisObjectSDS 结构。这样一来,内存分配只有一次,而且也避免了内存碎片。

// file: object.c

/* 
 * 创建一个编码为 OBJ_ENCODING_RAW 的字符串对象,这是一个普通字符串对象,其中 o->ptr 指向正确的 sds 字符串。
 * 
 * @param *ptr
 * @param len
 */
robj *createRawStringObject(const char *ptr, size_t len) {
    // 创建一个字符串对象 type是OBJ_STRING  encoding是OBJ_ENCODING_RAW  长度是字符串长度
    return createObject(OBJ_STRING, sdsnewlen(ptr,len));
}

在创建普通字符串时,Redis需要分别给 redisObjectSDS 分别分配一次内存,这样就既带来了内存分配开销,同时也会导致内存碎片。

(5.2.1) 为什么EMBSTR的大小要<=44

44是因为 N = 64(CPU缓存行大小) - 16(redisObject结构体占用内存大小) - 3(sdshr8结构体占用内存大小) - 1(结束符大小'\0'), N = 44 字节。
那么为什么是64减呢,为什么不是别的,CPU访问内存读取数据时以cache line为单位,在目前的x86体系下,一般的缓存行大小是64字节,如果整个结构体起始地址64字节对齐,一次内存IO就可以读取全部数据,redis为了一次能加载完成,因此采用64自己作为embstr类型(保存redisObject)的最大长度。


(6) 命令的类型检查和多态

有了 redisObject 结构的存在, 在执行处理数据类型的命令时, 进行类型检查和对编码进行多态操作就简单得多了。

当执行一个处理数据类型的命令时, Redis 执行以下步骤:

  1. 根据给定 key ,在数据库字典中查找和它相对应的 redisObject ,如果没找到,就返回 NULL 。
  2. 检查 redisObject 的 type 属性和执行命令所需的类型是否相符,如果不相符,返回类型错误。
  3. 根据 redisObject 的 encoding 属性所指定的编码,选择合适的操作函数来处理底层的数据结构。
  4. 返回数据结构的操作结果作为命令的返回值。

比如在 list 类型上使用 get命令会提示 WRONGTYPE

127.0.0.1:6379> lpush key_list_msg msg_1
(integer) 1
127.0.0.1:6379>
127.0.0.1:6379> get key_list_msg
(error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379>
127.0.0.1:6379> llen  key_list_msg
(integer) 1
127.0.0.1:6379>
// file: src/object.c

int checkType(client *c, robj *o, int type) {

    // 如果类型不一致
    if (o->type != type) {
        addReply(c,shared.wrongtypeerr); // 返回
        return 1;
    }
    return 0;
}

(7) 对象共享

有一些对象在 Redis 中非常常见, 比如命令的返回值 OK、ERROR、WRONGTYPE 等字符, 另外,一些小范围的整数,比如个位、十位、百位的整数都非常常见。

为了利用这种常见情况,Redis在内部使用了享元模式(Flyweight Pattern),通过预分配一些常见的值对象,并在多个数据结构之间共享这些对象,避免了重复分配的麻烦,也节约了一些CPU时间。

// file: src/server.c 

void createSharedObjects(void) {

    // 共享整数型redisObject  默认0-~10000
    for (j = 0; j < OBJ_SHARED_INTEGERS; j++) {
        // 
        shared.integers[j] =
            makeObjectShared(createObject(OBJ_STRING,(void*)(long)j));
        // redisObject编码设置为int     
        shared.integers[j]->encoding = OBJ_ENCODING_INT;
    }

}

(8) 引用计数以及对象的销毁

当将redisObject用作数据库的键或者值,而不是用来储存参数时,对象的生命期是非常长的,因为C语言本身没有自动释放内存的相关机制。

另一方面,一个共享对象可能被多个数据结构所引用,这时像是”这个对象被引用了多少次?”之类的问题就会出现。

为了解决以上两个问题, Redis 的对象系统使用了引用计数技术来负责维持和销毁对象, 它的运作机制如下:

  1. 每个 redisObject 结构都带有一个 refcount 属性,指示这个对象被引用了多少次。
  2. 当新创建一个对象时,它的 refcount 属性被设置为 1 。
  3. 当对一个对象进行共享时,Redis 将这个对象的 refcount 增一。
  4. 当使用完一个对象之后,或者取消对共享对象的引用之后,程序将对象的 refcount 减一。
  5. 当对象的 refcount 降至 0 时,这个 redisObject 结构,以及它所引用的数据结构的内存,都会被释放。
// file: src/object.c

void decrRefCount(robj *o) {

    // 引用次数为1
    if (o->refcount == 1) { 
        switch(o->type) {
        case OBJ_STRING: freeStringObject(o); break;
        case OBJ_LIST: freeListObject(o); break;
        case OBJ_SET: freeSetObject(o); break;
        case OBJ_ZSET: freeZsetObject(o); break;
        case OBJ_HASH: freeHashObject(o); break;
        case OBJ_MODULE: freeModuleObject(o); break;
        case OBJ_STREAM: freeStreamObject(o); break;
        default: serverPanic("Unknown object type"); break;
        }
        zfree(o);
    } else {
        if (o->refcount <= 0) serverPanic("decrRefCount against refcount <= 0");
        if (o->refcount != OBJ_SHARED_REFCOUNT) o->refcount--;
    }
}

(9) 总结

1、要想理解 Redis 数据类型的设计,必须要先了解 redisObject。

Redis 的 key 是 String 类型,但 value 可以是很多类型(String/List/Hash/Set/ZSet等),所以 Redis 要想存储多种数据类型,就要设计一个通用的对象进行封装,这个对象就是 redisObject。

其中,最重要的 2 个字段:

  • type:面向用户的数据类型(String/List/Hash/Set/ZSet等)
  • encoding:每一种数据类型,可以对应不同的底层数据结构来实现(SDS/ziplist/intset/hashtable/skiplist等)

例如 String,可以用 embstr(嵌入式字符串,redisObject 和 SDS 一起分配内存),也可以用 rawstr(redisObject 和 SDS 分开存储)实现。

又或者,当用户写入的是一个「数字」时,底层会转成 long 来存储,节省内存。

同理,Hash/Set/ZSet 在数据量少时,采用 ziplist 存储,否则就转为 hashtable 来存。

所以,redisObject 的作用在于:

  1. 为多种数据类型提供统一的表示方式
  2. 同一种数据类型,底层可以对应不同实现,节省内存
    3)支持对象共享和引用计数,共享对象存储一份,可多次使用,节省内存

redisObject 更像是连接「上层数据类型」和「底层数据结构」之间的桥梁。

2、关于 String 类型的实现,底层对应 3 种数据结构:

  • embstr:小于等于 44 字节,嵌入式存储,redisObject 和 SDS 一起分配内存,只分配 1 次内存
  • rawstr:大于 44 字节,redisObject 和 SDS 分开存储,需分配 2 次内存
  • long:整数存储(小于 10000,使用共享对象池存储,但有个前提:Redis 没有设置淘汰策略,详见 object.c 的 tryObjectEncoding 函数)

3、ziplist 的特点:

  1. 连续内存存储:每个元素紧凑排列,内存利用率高
  2. 变长编码:存储数据时,采用变长编码(满足数据长度的前提下,尽可能少分配内存)
    3)寻找元素需遍历:存放太多元素,性能会下降(适合少量数据存储)
  3. 级联更新:更新、删除元素,会引发级联更新(因为内存连续,前面数据膨胀/删除了,后面要跟着一起动)
    List、Hash、Set、ZSet 底层都用到了 ziplist。

4、intset 的特点:

  1. Set 存储如果都是数字,采用 intset 存储
  2. 变长编码:数字范围不同,intset 会选择 int16/int32/int64 编码(intset.c 的 _intsetValueEncoding 函数)
    3)有序:intset 在存储时是有序的,这意味着查找一个元素,可使用「二分查找」(intset.c 的 intsetSearch 函数)
  3. 编码升级/降级:添加、更新、删除元素,数据范围发生变化,会引发编码长度升级

了解一下jemalloc 分配内存机制,jemalloc 为了方便管理,在每次分配内存的时候都会返回2的幂次的空间大小,比如我需要分配5字节空间,jemalloc 会返回8字节,15字节会返回16字节。其常见的分配空间大小有: 8, 16, 32, 64, …, 2kb, 4kb, 8kb。

但是这种方式也可能会造成,空间的浪费,比如我需要33字节,结果给我64字节,为了解决这个问题jemalloc将内存分配划分为,小内存(small_class)和大内存(large_class)通过不同的内存大小使用不同阶级策略。

参考资料

[1] Redis设计与实现-对象处理机制
[2] Redis源码剖析与实战 - 04 内存友好的数据结构该如何细化设计?
[3] Redis源码-github