Redis对象redisObject

一,Redis对象概述

我们知道Redis中使用了,简单动态字符串,双端链表,跳跃表,字典,压缩列表,整数集合,快速列表等数据结构,但是对于Redis,它并没用直接使用这些数据结构来实现键值对数据库,而是以它们为基础创建了一个对象系统。

    1.1  这个对象系统包括了字符串对象、列表对象、哈希对象、集合对象和有序集合对象,这五个类型的对象。这些对象的实现都是基于上述的数据结构实现的。基于五个不同对象的键值对数据库,让Redis能够在执行对给出的命令进行判断,相应对象能否执行该命令(比如LPUSHLLEN只能用于列表类型的键另外一些命令, 比如 DELTTLTYPE, 可以用于任何类型的键, 但是, 要正确实现这些命令, 必须为不同类型的键设置不同的处理方式: 比如说, 删除一个列表键和删除一个字符串键的操作过程就不太一样.。同时,对于不同情景下的不同对象还可以使用不同的数据结构进行实现,优化了对象在不同场景下的使用效率

    1.2 命令的类型检查和多态  (https://segmentfault.com/a/1190000019980165)

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

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

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

作为例子,以下展示了对键 key 执行 LPOP 命令的完整过程:

Redis对象redisObject_第1张图片

    1.3  Redis对象系统还实现了基于引用计数技术的内存回收机制这一机制的实现使得Redis可以在不使用某个对象时及时的将其自动释放同时还可以使得多个数据库键共享一个值对象来节约内存

    1.4 Redis还在对象系统中实现了访问时间的记录。


二,对象类型

每次使用Redis数据库时,Redis会使用对象来表示数据库中的键和值。一个键值对就是两个对象,键是键对象,值是值对象。

2.1、Redis中的对象(redisObject)结构源码

推荐文章https://github.com/menwengit/redis_source_annotation

#define LRU_BITS 24
#define LRU_CLOCK_MAX ((1<lru */
#define LRU_CLOCK_RESOLUTION 1000 /* LRU clock resolution in ms */
 
typedef struct redisObject {
    //对象的数据类型,占4bits,共5种类型
    unsigned type:4;        
    //对象的编码类型,占4bits,共10种类型
    unsigned encoding:4;
 
    //least recently used
    //实用LRU算法计算相对server.lruclock的LRU时间
    unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */
 
    //引用计数
    int refcount;
 
    //指向底层数据实现的指针
    void *ptr;
} robj;
 
//type的占5种类型:
/* Object types */
#define OBJ_STRING 0    //字符串对象
#define OBJ_LIST 1      //列表对象
#define OBJ_SET 2       //集合对象
#define OBJ_ZSET 3      //有序集合对象
#define OBJ_HASH 4      //哈希对象
 
/* 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. */
// encoding 的10种类型
#define OBJ_ENCODING_RAW 0     /* Raw representation */     //原始表示方式,字符串对象是简单动态字符串
#define OBJ_ENCODING_INT 1     /* Encoded as integer */         //long类型的整数
#define OBJ_ENCODING_HT 2      /* Encoded as hash table */      //字典
#define OBJ_ENCODING_ZIPMAP 3  /* Encoded as zipmap */          //不再使用
#define OBJ_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */  //双端链表,不再使用
#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 */   //embstr编码的简单动态字符串
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */   //由压缩列表组成的双向列表-->快速列表

2.2、对象类型

redisObject结构中的type属性记录对象的属性。有如下五种类型:

类型常量 对象
OBJ_STRING   0 字符串对象
OBJ_LIST  1 列表对象
OBJ_SET  2 集合对象
OBJ_ZSET 3 有序集合对象
OBJ_HASH 4

哈希对象

在Redis中键经常是一个字符串对象,而值却可以是以上五大类型中的一种。我们可以使用TYPE命令来查看值对象的对象类型。

Redis对象redisObject_第2张图片

这里我们可以看到,键str对应的值对象为字符串类型,键number对应的是列表对象。

2.3、编码类型

redisObject结构中的ptr指针指向对象底层实现的数据结构,而这些数据结构由redisObject中的encoding决定。用encoding记录不同对象使用的不同编码类型,而不同的编码类型就代表了不同的数据结构。

Redis对象redisObject_第3张图片

这里我们可以使用OBJECT ENCODING命令查看一个数据库对象键的值对象的编码方式。

Redis对象redisObject_第4张图片

这里的键str对应的值对象为字符串对象它采用的是embstr编码的简单动态字符串实现。同理,这里的键number对应的值对象为列表对象它的编码方式为quicklist(我使用的是Redis5.0版本)


三,对象的创建与释放

3.1 创建

robj *createObject(int type, void *ptr);
    robj *createStringObject(char *ptr, size_t len);
    robj *createStringObjectFromLongLong(long long value);
    robj *createStringObjectFromLongDouble(long double value, int humanfriendly);
    robj *createListObject(void);
    robj *createZiplistObject(void);
    robj *createSetObject(void);
    robj *createIntsetObject(void);
    robj *createHashObject(void);
    robj *createZsetObject(void);
    robj *createZsetZiplistObject(void);

createXXXObject的方法主要通过调用底层结构的创建方法来创建一个robj对象,例如createZiplistObject方法调用了ziplistNew方法创建一个ziplist然后赋值给robj.ptr指针。其它的就不一一赘述。

/* 创建一个ziplist对象 */
robj *createZiplistObject(void) {
    unsigned char *zl = ziplistNew();
    robj *o = createObject(REDIS_LIST,zl);
    o->encoding = REDIS_ENCODING_ZIPLIST;
    return o;
}

3.2 释放

void freeStringObject(robj *o);
void freeListObject(robj *o);
void freeSetObject(robj *o);
void freeZsetObject(robj *o);
void freeHashObject(robj *o);

对象的释放也是通过调用底层结构的方法来释放资源的,不同的是在释放前需要判断该对象的具体编码方式(然后进行相应的释放操作),如freeListObject:

/* 释放一个list对象 */
    void freeListObject(robj *o) {
        // list有两种不同的实现,根据不同的实现释放资源
        switch (o->encoding) {
        case REDIS_ENCODING_LINKEDLIST:
            listRelease((list*) o->ptr);
            break;
        case REDIS_ENCODING_ZIPLIST:
            zfree(o->ptr);
            break;
        default:
            redisPanic("Unknown list encoding type");
        }
    }

四、编码转换

什么是编码转换呢?

前面我们知道每一个对象都有不同的编码方式实现。而每个对象之间的不同的编码方式之间可以转换,这就是编码转换。

至于什么时候进行编码转换呢?

当然就是不满足某一个编码方式的使用情景时,就进行编码转换。

如:字符串对象有三种编码方式(int,embstr,raw)。而其中int、embstr编码会在不满足其使用条件时转为raw编码方式。我们知道,int(OBJ_ENCODING_INT)编码方式的使用情景:如果字符串对象保存的是整数值,且这个整数值可以用long类型表示就使用int编码方式。而通过APPEND命令在保存整数值的字符串对象后面加一个字符串,程序就会将原本的保存的整数变为字符串保存,编码方式也由int编码变为了raw编码。

Redis对象redisObject_第5张图片

同理,其他类型的编码方式在不满足其使用情景时也将进行编码转换。


五、内存回收机制

我们知道,C语言本身不像Java有内存自动回收机制。但是,在开始的时候曾说到Redis的对象系统采用计数技术实现了内存回收机制。Redis也通过这一机制通过跟进对象的引用计数信息,在适当的时候进行内存回收。而计数信息在redisObject结构中由refcount属性记录。

  • 当新建一个对象时,refcount的值被初始化为1.
  • 当对象被一个新程序引用时,refcount+1.
  • 当对象不再被一个程序引用时,refcount-1.

最终当refcount为0时,对象占的内存被释放。


六、对象共享

通过计数技术实现的不仅仅是内存回收机制,还有对象共享。

何为对象共享?

对象共享指的是,当创建了一个键A时,它的值对象保存整数100。而这时再创建一个键B时,同样值对象保存为1整数100。这时Redis就会让键A,B共享保存100的值对象。

Redis对象redisObject_第6张图片

键A,B共有值对象100

Redis对象redisObject_第7张图片

Redis中实现对象共享步骤

    将数据库键的值指针指向一个现有的值对象
    将被共享的值对象的refcount属性+1.

对象共享能够节约内存。共享对象越多节约效果越显著。

在Redis服务器启动时,会创建一万个字符串对象,这些字符串对象包含了0—9999的整数,因此int编码的字符串对象无需再创建,只需要共用即可。


七,不同的对象对应编码方式简介

7.1、字符串对象

字符串对象的编码方式有int、raw、embstr。三种不同的类型对应不同的使用情景,且之间可以相互转换。

1. int(OBJ_ENCODING_INT)编码方式的使用情景:如果字符串对象保存的是整数值,且这个整数值可以用long类型表示就使用int编码方式。(注:1、字符串对象会将整数保存到ptr属性中去。2、long double类型在Redis中以字符串类型存储。3、保存浮点数会将其转换为字符串再进行保存,使用时将其转换回浮点数。)。
Redis对象redisObject_第8张图片

2. raw(OBJ_ENCODING_RAW)编码方式的使用情景:如果字符串对象保存的是一个字符串并且这个字符串长度大于32字节,那么就使用编码方式为raw的简单动态字符串来保存。其具体结构如下

struct sdshdr {
    //字符串长度
    int len;
    // buf中未使用的字节数
    int free;
    // 字节数组,用于保存字符串
    char buf[];
}

Redis对象redisObject_第9张图片

3,embstr(OBJ_ENCODING_EMBSTR)编码方式的使用情景:如果值为字符串对象,保存的是一个字符串并且这个字符串的长度小于32字节,那么就将使用编码方式为embstr的简单动态字符串保存。我们用OBJECT ENCODING命令查看编码方式:

4,embstr编码和raw编码的简单动态字符串的区别

embstr和raw都是简单动态字符串的编码方式,两者有什么不同吗?

(1)、首先从保存的字符串长度由就可见不同。embstr是专门用于保存短字符串的一种优化编码方式。

(2)、采用内存分配方式不同,虽然raw和embstr编码方式都是使用redisObject结构和sdshdr结构。但是raw编码方式采用两次分配内存的方式,分别创建redisObject和sdshdr,而embstr编码方式则是采用一次分配,分配一个连续的空间给redisObject和sdshdr。(embstr一次性分配内存的方式:1,使得分配空间的次数减少。2、释放内存也只需要一次。3、在连续的内存块中,利用了缓存的优点。)
Redis对象redisObject_第10张图片

7.2、列表对象

列表对象的底层实现在3.2之前的版本采用的是ziplist和linkedlist编码方式,在3.2后的版本采用的是quicklist编码方式。我这里使用的是Redis5.0,已经是quicklist为底层实现了

7.3、哈希对象

哈希对象的编码可以是ziplist(OBJ_ENCODING_ZIPLIST)或hashtable(OBJ_ENCODING_HT)。两种编码方式可以相互转换。

ziplist编码方式使用情景:哈希对象所保存的所有键值对的键和值的字符串长度不能大于64字节。并且哈希对象所保存的键值对数量小于512个。当不符合ziplist编码情景的时候就用hashtable编码

7.4、集合对象

集合对象的编码方式有inset(OBJ_ENCODING_INSET)和hashtable(OBJ_ENCODING_HT),可以转换。但一般都是inset编码方式转为hashtable编码方式。

inset编码方式使用情景:集合对象使用整数集合作为底层实现,集合包含的所有元素都被保存在整数集合中。并且集合对象保存的元素数量不得超过512个。当不满足inset编码方式时就采用hashtable编码方式

7.5 有序集合对象

有序集合采用的编码方式为ziplist和skiplist。两者可以转换。

ziplist编码方式使用情景:有序集合保存元素小于128时,并且集合中保存的元素长度小于64字节。不满足上述情况就使用skiplist编码方式。

 

 

你可能感兴趣的:(redis)