关于redis:Redis对象处理机制

42次阅读

共计 9163 个字符,预计需要花费 23 分钟才能阅读完成。

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

比如说,LPUSH 和 LLEN 只能用于列表键,如果用错了会提醒 (error) WRONGTYPE Operation against a key holding the wrong kind of value

比方在 string 类型上用了 llen 就会提醒这个谬误,那么 redis 是怎么做到发现类型谬误并提醒 WRONGTYPE 的?

127.0.0.1:6379> set key1 value1
OK
127.0.0.1:6379> get key1
"value1"
127.0.0.1:6379>
127.0.0.1:6379>  llen key1
(error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379>

实际上,Redis 每个键都带有类型信息,使得程序能够查看键的类型,并为它抉择适合的解决形式。

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

(1) Redis 对象零碎

为了应答不同的对象类型 (type) 和对象编码(encoding),Redis 构建了本人的类型零碎,这个零碎的次要性能包含:

redisObject 对象。
基于 redisObject 对象的类型查看。
基于 redisObject 对象的显式多态函数。
对 redisObject 进行调配、共享和销毁的机制。

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

redisObject 是 Redis 类型零碎的外围,数据库中的每个键、值,以及 Redis 自身解决的参数,都示意为这种数据类型。

(1.1) redisObject 构造定义

Redis 应用的根本数据对象构造体 redisObject

源码: https://github.com/redis/redi…

<!–more–>

// file: server.h 

/**
 * redis 对象
 */
typedef struct redisObject {
    unsigned type:4;  // 数据类型  4 个 bits  面向使用者的数据类型(string / list / hash / set / zset 等)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;

(1.2) redisObject 的作用

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

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

redisObject 的作用在于:

  1. 为多种数据类型提供对立的示意形式
  2. 同一种数据类型,底层能够对应不同实现,节俭内存
  3. 反对对象共享和援用计数,共享对象存储一份,可屡次应用,节俭内存

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

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

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

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

#define OBJ_MODULE 5    /* Module object. */
#define OBJ_STREAM 6    /* Stream object. */

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

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

/* 
 * 对象编码。* 某些类型的对象(如字符串和哈希)能够在外部以多种形式示意。* 对象的 "encoding" 字段设置为此对象的此字段之一。*
 * Objects encoding. 
 * Some kind of objects like Strings and Hashes can be internally represented in multiple ways. 
 * The 'encoding' field of the object is set to one of this fields for this object. 
 */
#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 */

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

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

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

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

依据给定 key,在数据库字典中查找和它绝对应的 redisObject,如果没找到,就返回 NULL。
查看 redisObject 的 type 属性和执行命令所需的类型是否相符,如果不相符,返回类型谬误。
依据 redisObject 的 encoding 属性所指定的编码,抉择适合的操作函数来解决底层的数据结构。
返回数据结构的操作后果作为命令的返回值。

比方在 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>

(1.6) 对象共享

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

为了利用这种常见状况,Redis 在外部应用了享元模式(Flyweight Pattern),通过预调配一些常见的值对象,并在多个数据结构之间共享这些对象,防止了反复调配的麻烦,也节约了一些 CPU 工夫。

(1.7) 援用计数以及对象的销毁

当将 redisObject 用作数据库的键或者值,而不是用来贮存参数时,对象的生命期是十分长的,因为 C 语言自身没有主动开释内存的相干机制。

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

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

每个 redisObject 构造都带有一个 refcount 属性,批示这个对象被援用了多少次。
当新创建一个对象时,它的 refcount 属性被设置为 1。
当对一个对象进行共享时,Redis 将这个对象的 refcount 增一。
当应用完一个对象之后,或者勾销对共享对象的援用之后,程序将对象的 refcount 减一。
当对象的 refcount 降至 0 时,这个 redisObject 构造,以及它所援用的数据结构的内存,都会被开释。

(2) 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;

(2.1) 位域定义方法

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

变量后应用冒号和数值的定义方法。是 C 语言中的位域定义方法,能够用来无效地节俭内存开销。

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

(2.2) 嵌入式字符串

SDS 在保留比拟小的字符串时,会应用嵌入式字符串的设计办法,将字符串间接保留在 redisObject 构造体中。而后在 redisObject 构造体中,存在一个指向值的指针 ptr,而一般来说,这个 ptr 指针会指向值的数据结构。

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

// file: object.c

/* 
 * 如果小于 OBJ_ENCODING_EMBSTR_SIZE_LIMIT,则应用 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 编码
}
// file: 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);

    // 
    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 构造。这样一来,内存调配只有一次,而且也防止了内存碎片。

// 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));
}
// file: object.c

robj *createObject(int type, void *ptr) {
    // 为 redisObject 构造体分配内存空间
    robj *o = zmalloc(sizeof(*o));
    // 设置 redisObject 的类型
    o->type = type;
    // 设置 redisObject 的编码类型,此处是 OBJ_ENCODING_RAW,示意惯例的 SDS
    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;
}

在创立一般字符串时,Redis 须要别离给 redisObjectSDS 别离调配一次内存,这样就既带来了内存调配开销,同时也会导致内存碎片。

为什么 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) 的最大长度。

评论

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)寻找元素需遍历:寄存太多元素,性能会降落(适宜大量数据存储)
4) 级联更新:更新、删除元素,会引发级联更新(因为内存间断,后面数据收缩 / 删除了,前面要跟着一起动)

List、Hash、Set、ZSet 底层都用到了 ziplist。

4、intset 的特点:

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

课后题:SDS 判断是否应用嵌入式字符串的条件是 44 字节,你晓得为什么是 44 字节吗?

嵌入式字符串会把 redisObject 和 SDS 一起分配内存,那在存储时构造是这样的:

  • redisObject:16 个字节
  • SDS:sdshdr8(3 个字节)+ SDS 字符数组(N 字节 + \0 结束符 1 个字节)

Redis 规定嵌入式字符串最大以 64 字节存储,所以 N = 64 – 16(redisObject) – 3(sdshr8) – 1(\0),N = 44 字节。

理解一下 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

Redis 源码分析与实战 学习笔记 Day4 内存敌对的数据结构该如何细化设计?https://time.geekbang.org/col…

正文完
 0