十二张图详解Redis的数据结构和对象系统(汇总篇)

435 阅读8分钟

多图解释Redis的整数集合intset升级过程

内存节省到极致!!!Redis中的压缩表,值得了解...

前言

hello,大家好,又见面啦😊。

前面几周我们一起看了Redis底层数据结构,如动态字符串SDS双向链表Adlist字典Dict跳跃表整数集合intset压缩表ziplist,如果有对Redis常见的类型或底层数据结构不明白的请看上面传送门。

经过长达几周的学习(折磨),终于到了我们正文。Redis并没有直接使用上述的几种底层数据结构来实现真正的键值对数据库,而是基于这些数据结构创建了对象系统

简单来说,即前面说过的几种基本类型统一弄一个大的数据结构套住,底层数据结构对客户不可见。

对象的结构RedisObject

结构图解

对象系统RedisObject主要包括五个部分,分别是基本类型type(字符串string,列表list,哈希hash,集合set,有序集合zset),编码格式encoding(raw,int,embstr,双向链表adlist,字典hashtable,跳跃表ziplist,整数集合intset,压缩表ziplist),过期时钟LRU,引用次数refcount,数据指针*ptr。具体如下图所示,如果对过期时钟LRU和引用次数refcount不明白的,下面会详细说明,不慌哈。

分析

基本类型type和编码格式encoding这两个就不说了,如果不知道,可以翻看我之前的文章。

过期时钟LRU:LRU是Least Recently Used的缩写,即最近最少使用,是内存管理的一种页面置换算法。算法的核心是:如果一个数据在最近一段时间内没有被访问,那么他在将来被访问的可能性也很小。换句话说,当内存达到极限的时候,应该把内存中最久没有被访问的数据淘汰掉。所以RedisObject中引入了LRU时钟,每个对象每次被访问的时候,都会记录下当前服务器的LRU时钟,然后用服务器的LRU时钟来减去对象本身的时钟,得到的就是这个对象没有被访问的时间间隔(即空闲时间),空闲时间最大的就是需要淘汰的对象。

举个例子,有两个对象A,B,他们的空闲时间分别是5s,10s,这意味着A是5s前被使用过,而B是10s前被使用过,当内存不够的时候,会优先淘汰空闲时间大的对象,即优先淘汰对象B。

引用次数refcount:我们都知道管理对象的生命周期有两种方法,1是根搜索法(即可达性,Java的JVM用的就是这个,我们之后说JVM的时候再说哈),2是引用计数。Redis就使用了第二种引用计数的实现。refcount就是记录该对象被引用的次数,类型为整型。当创建新对象时,refcount初始化为1,当有新程序使用该对象时,refcount加1,当对象不再被新程序使用时,refcount减1,当refcount等于0,对象占用的内存会被释放。所以refcount的作用就是用于内存回收。

/* 引用值refcount递增 */
void incrRefCount(robj *o) {
    o->refcount++;
}

/* 引用值refcount递减 */
void decrRefCount(robj *o) {
    //refcount小于等于0,抛出异常
    if (o->refcount <= 0) redisPanic("decrRefCount against refcount <= 0");
    //refcount等于1,根据类型type释放资源
    if (o->refcount == 1) {
        switch(o->type) {
            case REDIS_STRING: freeStringObject(o); break;
            case REDIS_LIST: freeListObject(o); break;
            case REDIS_SET: freeSetObject(o); break;
            case REDIS_ZSET: freeZsetObject(o); break;
            case REDIS_HASH: freeHashObject(o); break;
            default: redisPanic("Unknown object type"); break;
        }
        zfree(o);
    } else {
    	//引用值refcount递减
        o->refcount--;
    }
}

源码

关于上面针对redisobject的图解,源码来了哈,是不是看到熟悉的小伙伴啦。

typedef struct redisObject {
    //五种基本类型,string,list,hash,set,zset
    unsigned type:4;
    //编码格式
    unsigned encoding:4;
    //实用LRU算法
    unsigned lru:LRU_BITS; 
    //引用计数
    int refcount;
    //指向底层数据实现的指针
    void *ptr;
} robj; 

/* 五种基本类型的宏定义 */
#define OBJ_STRING 0
#define OBJ_LIST 1
#define OBJ_SET 2
#define OBJ_ZSET 3
#define OBJ_HASH 4

/*编码格式的宏定义*/
#define OBJ_ENCODING_RAW 0     /* string类型的Raw */
#define OBJ_ENCODING_INT 1     /* string类型的int */#define OBJ_ENCODING_HT 2      /* 字典hashtable*/
#define OBJ_ENCODING_ZIPMAP 3  
#define OBJ_ENCODING_LINKEDLIST 4 
#define OBJ_ENCODING_ZIPLIST 5 /* 压缩表ziplist */
#define OBJ_ENCODING_INTSET 6  /* 整数集合intset */
#define OBJ_ENCODING_SKIPLIST 7  /* 跳跃表skiplist */
#define OBJ_ENCODING_EMBSTR 8  /*string类型的embstr */
#define OBJ_ENCODING_QUICKLIST 9 /* 快速表quicklist */

为什么要设计

1.自由改进内部编码格式,对外的数据结构和命令不发生影响,能够做到对用户透明化。如果开发出更优秀的代码,可以及时替换,而对用户来说,不需要做任何改变。

可以试想下,如果我们针对内部编码格式encoding编码,那么redis做一点点改变,我们都需要改代码,这硬编码多么难受。

2.多种内部编码格式可以在不同场景下发挥各自的优势,比如压缩列表比较节省内存,但是在元素多的清空下,性能有所降低,这时候Redis会在内部将压缩列表改成双向链表。

我们可以当起甩手掌柜,一切都交给Redis,太幸福啦。

下面的底层实现,可以看我之前针对每种类型的具体文章。

字符串的底层实现

obj_encoding_int:用来存放整数的字符串对象

obj_encoding_raw:简单动态字符串实现的字符串对象

obj_encoding_embstr:embstr编码的简单动态字符串实现的字符串对象

列表的底层实现

任意门:Redis的双向链表一文全知道

obj_encoding_quicklist:快速表实现的列表对象

obj_encoding_ziplist:压缩表实现的列表对象

哈希的底层实现

任意门:内存节省到极致!!!Redis中的压缩表,值得了解...

任意门:面试官:说说Redis的Hash底层 我:......(来自阅文的面试题)

obj_encoding_ziplist:压缩列表实现哈希对象

obj_encoding_ht:字典实现hash对象

集合的底层实现

任意门:多图解释Redis的整数集合intset升级过程

obj_encoding_ht:字典实现的集合对象

obj_encoding_intset:整数集合实现的集合对象

有序集合的底层实现

任意门:跳跃表确定不了解下😏

obj_encoding_ziplist:跳跃表和字典实现的有序集合对象

obj_encoding_skiplist:压缩列表实现的有序集合对象

对象系统的操作

创建一个字符串对象

在之前已经说过string类型是封装出来的,实际是有raw和embstr两种,接下来我们就来看下这两种。

  • 编码为OBJ_ENCODING_RAW

    robj *createObject(int type, void *ptr) {
    //尝试分配空间
    robj *o = zmalloc(sizeof(*o));
    //设置类型
    o->type = type;
    //设置编码格式为OBJ_ENCODING_RAW
    o->encoding = OBJ_ENCODING_RAW;
    //设置指针
    o->ptr = ptr;
    //引用次数初始化为1
    o->refcount = 1;
    //设置过期时间
    if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
    o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
    } else {
    o->lru = LRU_CLOCK();
    }
    return o; }

  • 编码为OBJ_ENCODING_EMBSTR

    robj *createEmbeddedStringObject(const char *ptr, size_t len) { //分配空间 robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1); //o加1,正好是sdshdr8的地址 struct sdshdr8 sh = (void)(o+1);

    //类型为字符串对象 o->type = OBJ_STRING; //设置编码类型OBJ_ENCODING_EMBSTR o->encoding = OBJ_ENCODING_EMBSTR; //设置编码类型OBJ_ENCODING_EMBSTR o->ptr = sh+1; //初始化引用计数 o->refcount = 1; //设置过期时间 if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) { o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL; } else { o->lru = LRU_CLOCK(); } //设置编码类型OBJ_ENCODING_EMBSTR sh->len = len; //设置最大容量 sh->alloc = len; //设置sds的类型 sh->flags = SDS_TYPE_8; //如果传了字符串参数 if (ptr) { //传进来的ptr保存到对象中 memcpy(sh->buf,ptr,len); sh->buf[len] = '\0'; } else { //否则将对象的空间初始化为0 memset(sh->buf,0,len+1); } //返回首地址 return o; }

关于Redis是如何选择,通过下面的代码,我们可以发现选择是通过字符串长度的判断来实现的。

  • 两种字符串对象编码方式的区别

    #define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44 // 创建字符串对象,根据长度使用不同的编码类型 // createRawStringObject和createEmbeddedStringObject的区别是: // createRawStringObject是当字符串长度大于44字节时,robj结构和sdshdr结构在内存上是分开的 // createEmbeddedStringObject是当字符串长度小于等于44字节时,robj结构和sdshdr结构在内存上是连续的 robj *createStringObject(const char *ptr, size_t len) { if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) return createEmbeddedStringObject(ptr,len); else return createRawStringObject(ptr,len); }

引用计数管理对象

添加引用:

//引用计数加1
void incrRefCount(robj *o) {
    o->refcount++;
}

去除引用:

 //引用计数减1
void decrRefCount(robj *o) {
    if (o->refcount <= 0) serverPanic("decrRefCount against refcount <= 0");

    //当引用对象等于1时,在操作引用计数减1,直接释放对象的ptr和对象空间
    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;
        default: serverPanic("Unknown object type"); break;
        }
        zfree(o);
    } else {
        o->refcount--;  //否则减1
    }
}

结语

该篇主要讲了Redis的RedisObject如何对基本数据类型进行封装,先从RedisObject是什么,剖析了其主要组成部分,进而描述了多种基本类型的封装区别,最后针对RedisObject的数据结构分析了对象是如何创建,优化,和引用的。

如果觉得写得还行,麻烦给个赞👍,您的认可才是我写作的动力!

如果觉得有说的不对的地方,欢迎评论指出。也可以关注我的公众号学习Java的小姐姐,我们一起讨论下。

好了,拜拜咯。

参考资料

blog.csdn.net/androidlush…