1 、概述
关于redis数据存储,涉及到内存分配器,简单动态字符串,5种对象类型的内部编码。
下图是执行set hello world的时候锁涉及到的内存分配
(1)dictentry: redis是key-value型的数据库,因此对每一个键值对都有一个dictEntry,里边存储着key和value的指针,next指向下一个dictEntry
(2)key :如图右上角可见,key(hello)并不是以直接字符串进行存储的,而是存储再sds的结构中
(3)redisObject:value(Object)既不是直接存储字符串,也不是直接存储在sds中,而是存储再redisObject中,实际上,不论value是五种类型的哪一种,都是通过redisObject种的type字段指明了value的数据类型,ptr字段则是指向对象所在的地址,不过显然字符串对象依然是存储在sds中
(4)jemalloc:无论是dictEntry对象还是redisObject sds对象,都是需要内存分配的。例如DicEntry对象,有3个指针组成,在64位的机器下占24个字节,jemmalloc就会为它分配32字节大小的内存单元。
2 、jemalloc
redis在编译的时候会制定内存分配器,一般有libc,jemalloc和tcmalloc。默认是jemalloc。
jemalloc作为redis默认的内存分配器,在减小内存碎片方面做得相对比较好,jemmalloc会把内存划分为小,大和巨大三个范围,每个范围又划分为许多小的内存块单位,当redis存储的时候,会选择大小合适的内存块进行存储。
jemalloc划分的内存单元如图:
例如,如果需要存储大小为130字节的对象,就会存入160字节单位的内存单元中。
3、redisObject
不论是哪一种数据类型,redis都不会直接存储,而是通过redisObject对象进行存储。redisObject对象非常重要,redis对象的类型,内部编码,内存回收,对象共享等功能,都需要redisObject的支持。
redisObject的定义如下,不同版本可能稍微有所不同:
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;
redisObject每个字段的含义如下:
-
type
type字段表示类型,占4个比特;目前包含的类型有字符串,列表,哈希,集合和有序集合。
当我们执行type命令的时候,可以通过redisobject的type字段获得对象的数据类型,如下图:
image.png -
encoding
encoding表示对象的内部编码,占4个比特。
对于redis支持的每种类型,至少内部都有两种编码,例如对于字符串,有int,embstr,raw三种编码。通过encoding属性,redis可以根据不同的使用场景来为对象设置不同的编码,大大的提高了redis的灵活性和效率。以列表为例,有压缩列表和双端列表两种,如果列表中的元素较少,redis倾向于使用压缩列表进行存储,应为压缩列表占用的内存更少,而且对比双端列表可以更快速的载入,当列表对象元素较多的时候,压缩列表就会转为更适合存储大量元素的双端列表。
通过Object encoding命令,可以查看对象采用的是什么编码,如下图:
image.png
五种对象的编码将在后续体现出来
-
lru
lru记录着对象最后一次被命令程序访问的时间,占内存大小不同的版本有所差异(4.0版本占24比特,2.6版本占22比特)。
通过对比lru的时间和当前时间,可以计算某个对象的空转时间,object idletime命令可以显示该空转时间(单位是秒)。object idletime命令的特殊之处在于不会改变对象的lru值。
image.png
lru值除了和object idletime命令打印之外,还和redis的内存回收有关系,如打开redis的maxmemory选项,切内存回收算法选择的是volatile-lru或者allkeys-lru,那么redis内存占用超过maxmemory指定的值时,redis就会优先选择空转时间长的对象进行释放。
- refcount
refcount与共享对象
refcount记录的是该对象被引用的次数,类型为整型,占用4个字节,refcount的作用,主要在于对象的引用计数和内存回收,当初创建新对象的时候,refcount初始值化为1,当有新的程序使用该对象,refcount加1,当对象不再被使用就会减一,知道refcount变为0,就会释放内存。
redis中很多refcount>1的对象,称为共享对象,redis为了节省空间,当一些对象重复出现,新的程序不会创建新的对象,而是使用原来的对象,这个对象就叫共享对象,目前共享对象仅仅支持整数和字符串对象
共享对象的具体实现
redis的共享对象目前只支持整数值和字符串对象,之所以如此,实际上是对内存和cpu的平衡,共享对象虽然会降低内存的消耗,但是判断两个对象是否相等却需要额外的时间,对于整数值,判断操作的复杂度为O(1);对于普通字符串,哦按段复杂度为O(n),对于哈希列表和集合等,判断复杂度为O(N^2).
虽然共享对象只能是整数值和字符串对象,但是五种类型都可以使用共享对象。
就目前的实现,redis服务器在初始化的时候,会创建10000个字符串对象,分别为0到9999的整数值,当redis需要使用值为0到9999的时候,可以直接使用共享对象,10000这个数字可以通过REDIS_SHARED_INTEGERS参数的调整来进行改变。
共享对象的应用次数可以通过 object refcount命令进行查看,如下图
image.png - ptr
ptr指针指向具体的数据,如前面的例子中。
4、SDS
redis没有直接使用C字符串作为默认的字符串表示,而是使用了SDS。SDS是简单动态字符串缩写。
(1)SDS结构
SDS结构如下图:
struct sdshdr{
int len;
int free;
char buf[];
}
其中,buf表示字节数组,用来存储字符串,len表示buf已使用的长度,free表示buf未使用的长度,下面是两个例子。
通过SDS的结构可以看出,buf数组的长度=free+len+1(1表示字符串结尾的空字符串);所以,一个sds结构所占的长度-free+len+9.
(2)SDS与C字符串的比较
SDS在C字符串的基础上加入了free和len字段,带来了很多的好处;
- 获取字符串长度,sds是o(1) C字符串是O(n)
- 缓冲区溢出:使用C字符串的API时,如果字符串长度增加而忘记重新分配内存,很容易造成缓存溢出;而sds由于记录了长度,相应的API在可能造成缓冲区溢出时会自动重新分配内存,杜绝了缓冲区溢出。
- 修改字符串时内存的重新分配:对于C字符串,如果要修改字符串,就必须重新分配内存,如果没有重新分配内存,字符串长度增大时会造成内存缓存区溢出,字符串长度缩小时会造成内存泄漏,而对于SDS,由于可以激励free和len。因此解除了字符串长度和空间数组长度之间的关联,可以在此基础上进行优化,
- 存二进制数据,sds可以存二进制数据,C字符串不可以。
(3)SDS和C字符串的应用
redis在存储对象时,一律使用sds代替C字符串,例如set helle world命令,hello和word都是以sds的形式存在的,而sadd myset member1 member2 member3命令,不论是键("myset")还是集合中的元素,多少以sds的形式存储,除了存储对象,sds还用于存储各种缓冲区。只有在字符串不改变的情况下,如打印日志才会用到C字符串。