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对象
集合的底层实现
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的小姐姐
,我们一起讨论下。
好了,拜拜咯。