Redis中的Key-Value 键值对的想必大家都用到过,我们一般在业务开发中,或多或少都会使用到Redis来进行数据的缓存、非关系存储、甚至当做消息队列,数据中转站等。其中Key-Value键值对的操作更是为大家广泛使用,不知道大家工作中,有没有遇到过需要对Redis 内存使用量进行评估。 前段时间,公司的redis一个集群内存告警,原来业务方估算的大约4GB的内存使用量,数据还未完全灌倒Redis中,就耗费了将近30GB,当时使用的就是Key-value 写。为什么估算的量和真正的使用量差距会这么大?Key-Value键值对在Redis中是以怎样的结构进行存储的呢?下面如无特殊说明,我们以Redis3.0版本进行分析。
首先看下Redis中Key-Value键值对是怎样存储的概览图,共涉及DictEntry、RedisObject、SDS 3种结构,字符串编码、对象共享机制,以及jemalloc 内存分配。
dictEntry 字典
Redis 通过对Key 进行Hash计算,然后锁定对应的hash槽(table),Hash槽指向对应的dictEntry,dictEntry持有key、val以及Hash冲突时链表的下个字典节点的指针,dictEntry结构占用8+8+8=24字节。
/*
* 哈希表节点
*/
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
redisObject
RedisObject结构持有数据对象的元数据和数据对象指针,其中type(4bit)标识该数据是哪种数据类型,redis目前定义了String、List、Set、Zset、Hash5中类型,用户也可以自己拓展。encoding(4bit)标识数据的编码类型,目前定义了raw、int、embstr、hashtable、zipmap、linkedList、ziplist、intset、skiplist 8种(redis后续版本有调整),今天聊的主要涉及type=String,encoding包含raw、int、embstr这几种。lru(24bit)对象最后一次的访问时间,主要用于redis lru、lfu淘汰策略使用。refcount 引用计数,当refcount 为0 ,标识对象可以释放。ptr指针,指向真正的数据对象。RedisObject结构占用(4+4+24)/8 +4+8 = 16字节。
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 对象最后一次被访问的时间
unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
// 引用计数
int refcount;
// 指向实际值的指针
void *ptr;
} robj;
SDS(Simple Dynamic String,简单动态字符串)
Redis实现了自定义的字符串结构,较与C语言字符串相比,可以高效的计算长度、执行追加操作等,几乎所有的 Redis 模块中都用了 sds。
/*
* 保存字符串对象的结构
*/
struct sdshdr {
// buf 中已占用空间的长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间
char buf[];
};
如上述结构所示,字符串对象结构体,占用4+4=8个字节,sds字符串使用/0结尾,占用1个字节,也就是计算一个sds字符串对象的所占用字节数是4+4+1+len+free=9+len+free。
字符串编码
字符串编码涉及到raw、int、embstr 三种编码类型,他们之间的区别主要如下图,embstr相对于raw,主要是redisobject 和sds字符串是一块连续的内存,目的是减少内存碎片。int编码格式将ptr指针(8字节)直接赋值与数据值,目的减少内存寻址。
对象共享
Redis服务启动后,会内置数值型的共享对象,默认情况下0~9999,如果value是数值型,且在这个范围内,那么直接使用共享对象替代redisObject 和sds,并将过程中的redisObject的refcount-1;共享对象的引用+1;
上图a的值2,属于共享对象,被引用了3次。
Jemalloc
Redis 默认采用Jemalloc内存分配器 (ptmalloc、tcmalloc和jemalloc 内存分配器的差异)jemalloc 在 64 位系统中,将内存空间划分为小、大、巨大三个范围;当redis申请内存时,jemalloc 会根据划分申请对应的大小的空间,例如当要申请12byte的空间,jemalloc会分配16byte。
验证环节
前言:
介绍完了上述3种数据结构和2中机制后,我们进行实战验证,看看Redis是怎么运用这些机制的(大家验证时,请使用单节点实例,cluster模式会有些差异)。Redis3.0 版本,当字符串的长度小于等于39时,会采用embstr格式,大于39则编码格式为raw。
我们先回顾下redis中Key-Value键值对存储的形式,根据这个图来计算键值对存储所消耗的内存空间。由图所示,dictEntry所占用空间是固定的 3*8(3个指针)=24-->32byte,key受sds内存分配机制影响(目测SDS字符串首次分配时,不会预分配空闲空间,即dsd->free = 0)
当value 是字符串时
set a 123456a
此时占用内存为32(dictEntry)+16(key sds)+16(value redisObject )+ 16(value sds) = 80byte,如下图,符合预期。
当value 是数值时
set a 1234567
预测 32(dictEntry)+16(key sds)+16(value redisObject) = 64byte
可结果却是80byte,这是为什么呢?原来在redis接收到请求后,会对请求入参value创建redisObject字符串,此时他的规则是大于39为raw编码,小于等于39位embstr编码,这种场景下,运行时的value是embstr编码格式,尽管他是数值型的,在setCommand函数中,会再次对value进行编码,此时会将redisObject的ptr指针直接赋值1234567 value值,但是应该是基于内存碎片的考虑,并未释放embstr编码格式下的sds字符串所占用的空间,因此计算公式应该为 32(dictEntry)+16(key sds)+16(value redisObject )+ 16(value sds) = 80byte。
当value是数值时,且数值小于范围0~10000内时
set a 1
这个场景在没看源码之前,也是困扰了好久,其实这里使用到了共享对象机制,value不占用额外的内存空间,因此占用内存为 32(dictEntry)+16(key sds) = 48byte
主要源码
-
当redis接收到请求后,会给key 和val创建redisObject(源码networking#processMultibulkBuffer方法中调用了createStringObject函数),作为运行中的数据对象(并不是存储对象),根据字符串长度与阈值39的关系,采用raw或者embstr编码。此时例子中的key=a,value = abcde 都是redisObject对象,且采用了embstr编码。
createStringObject源码如下
#define REDIS_ENCODING_EMBSTR_SIZE_LIMIT 39
robj *createStringObject(char *ptr, size_t len) {
if (len <= REDIS_ENCODING_EMBSTR_SIZE_LIMIT)
return createEmbeddedStringObject(ptr,len);
else
return createRawStringObject(ptr,len);
}
-
对value进行编码,由前面createStringObject 知晓,目前value只有raw 或者embstr 两种编码格式,此处主要是对value是数值的场景进行优化。
如果value在0~9999范围,则使用共享对象,那么最终该操作,将不会申请额外的内存。
如果不在0~9999范围,则将redisObject中的ptr指针直接转为value。
/* Try to encode a string object in order to save space */ // 尝试对字符串对象进行编码,以节约内存。 robj *tryObjectEncoding(robj *o) { long value; sds s = o->ptr; size_t len; /* Make sure this is a string object, the only type we encode * in this function. Other types use encoded memory efficient * representations but are handled by the commands implementing * the type. */ redisAssertWithInfo(NULL,o,o->type == REDIS_STRING); /* We try some specialized encoding only for objects that are * RAW or EMBSTR encoded, in other words objects that are still * in represented by an actually array of chars. */ // 只在字符串的编码为 RAW 或者 EMBSTR 时尝试进行编码 if (!sdsEncodedObject(o)) return o; /* It's not safe to encode shared objects: shared objects can be shared * everywhere in the "object space" of Redis and may end in places where * they are not handled. We handle them only as values in the keyspace. */ // 不对共享对象进行编码 if (o->refcount > 1) return o; /* Check if we can represent this string as a long integer. * Note that we are sure that a string larger than 21 chars is not * representable as a 32 nor 64 bit integer. */ // 对字符串进行检查 // 只对长度小于或等于 21 字节,并且可以被解释为整数的字符串进行编码 len = sdslen(s); if (len <= 21 && string2l(s,len,&value)) { /* This object is encodable as a long. Try to use a shared object. * Note that we avoid using shared integers when maxmemory is used * because every object needs to have a private LRU field for the LRU * algorithm to work well. */ if (server.maxmemory == 0 && value >= 0 && value < REDIS_SHARED_INTEGERS) { decrRefCount(o); incrRefCount(shared.integers[value]); return shared.integers[value]; } else { if (o->encoding == REDIS_ENCODING_RAW) sdsfree(o->ptr); o->encoding = REDIS_ENCODING_INT; o->ptr = (void*) value; return o; } }最后:一个渣渣程序员,上面是结合一些资料和源码的主观理解,如果有不对或者疑问的地方,欢迎留言探讨,让我们轻松完爆Redis!期待你的加入。