深入学习Redis(1):Redis内存模型

发布于:2024-05-05 ⋅ 阅读:(36) ⋅ 点赞:(0)

Redis的五个对象类型

字符串,哈希,列表,集合,有序集合

本节有关redis的内存模型

1.估算redis的内存使用情况

目前内存的价格比较的高,如果对于redis的内存使用情况能够进行计算,就可以选用合适的设备进行使用,可以有效的节省开支

2.优化内存占用

对于redis,选用合适的数据类型和编码,这样才能更好的利用redis现有的内存

3.分析解决问题

对于redis出现阻塞,内存占用等问题的时候,能够很快发现问题并解决

127.0.0.1:6379> info memory
# Memory
used_memory:1324768
used_memory_human:1.26M
used_memory_rss:11939840
used_memory_rss_human:11.39M
mem_fragmentation_ratio:9.30
mem_allocator:jemalloc-5.2.1

used_memory是redis分配器分配的内存总量,其中包括虚拟内存swap

used_memory_rss是redis内存占据操作系统的内存,和top/ps一致,包括内存和内存碎片

used_memory_human这个对于阅读比较的好

对于mem_fragmentation_ratio这个值来说的话,他是对于used_memory_rss/used_memory的比值,可以发现这个等式,如果redis启用了虚拟内存swap的话,这个比值就可能变成小于1,这个时候就需要对于redis进行问题排查,应为磁盘太慢了,对于内存增加的方法有,增加redis节点,对于redis的服务器内存进行增加,优化应用,也就是分为横向和纵向的区别

如果内存中并未存入什么数据,那么就会造成这个比值过大

mem_allocator这个参数是指当前使用的内存分配器的版本

Redis内存的划分

1.数据

数据是有内存分配器分配的内存,会被统计在used_memory中

五种类型,分别是字符串,哈希,列表,集合,有序集合,这五种类型是对外提供的,当然,在内部还有基数统计,位图,地理位置,对于对象需要进行redisObject,SDS的包装,才会被放到内存中去

2.进程本身需要的内存

redis本身的代码,例如代码,常量池,这些占据了几兆,这点空间可以忽略不计,然后应为不经过jemalloc的分配,所以不被记录到used_memory中

3.缓冲内存

缓冲内存包括客户端的缓冲区,复制积压缓冲区,AOF缓冲区等,其中客户端缓存客户端连接输入输出缓存;复制积压缓冲区用户部分复制功能,AOF缓冲区对于AOF进行重写,保存最近的写入命令。这部分是由jemalloc来分配的,所以记录到used_memory当中

4.内存碎片

对于数据进行频繁修改之后,数据之间的大小相差就很大,导致redis释放得空间在实际得物理内存中并没有释放,redis也无法合理利用,导致了内存碎片。内存碎片不会统计到used_memory当中。

内存碎片的产生是多方面的,和对于内存的操作,数据的特点等都有关,内存分配器也有关系,设计的好,内存碎片产生的也少,jemalloc在这方面做的很好

redis中的内存碎片很大的时候,可以进行安全重启,在重启之后,redis可以从备份的文件中读取数据,在内存中进行重排,为每个数据进行选择合适的内存单元,减小内存的碎片

redis数据的存储细节

前面提到,对于数据我们是需要对他进行封装,包装成redisObject,或者SDS

在这张图片中,可以看到dictEntry是基本的单位,每一个键值对都有一个dictEntry,然后就里面也有一个这样的指针,然后就指向过去,里面的key可以看到是存储在SDS结构中,然后就是值,这个值不是放在sds里面的,是存储在redisObject里面,然后type字段表面这是sds类型,然后ptr指针指向了sds对象所在的地址,里面存放着值,值也是需要sds结构进行存储的

之后就是jemalloc对于上面说的dictEntry对象、redisObject、SDS对象都进行了内存的分配,dictEntry这个对象有三个指针,也就是3*8字节为24字节,然后就向上取整,给他分配了一个32字节大小的内存单元

jemalloc

在redis对于内存分配器进行选择的时候,内存分配器有libc,jemalloc,tcmalloc,三个,当然默认的是jemalloc,这一个在64位系统中,对于内存空间进行了小中大的三个范围的划分,当然,在每一个范围内又进行了更加细致的划分,

就像这里如果需要存储一个字节的对象,那么就需要字节的内存单元中

redisObject

在redis中有五种对象,现在有八种,不管哪一种,redis都不会进行直接存储,都会对于redisObject对象进行存储

redisObject对象中包含了redis对象的类型,内部编码,内存回收,共享对象等功能,都需要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;

type,对象的类型,也就是那八个

直接使用type的命令

encoding 在字符串中有三种编码 int emmbstr raw

对于列表对象来说,元素少的时候,就是int型,如果元素多的话,会抓换成embstr类型

int是压缩列表,embstr是双端链表

lru记录的是对象最后一次被程序访问的时间,如果你想看,可以使用object idletime命令进行查看

这条指令并不会修改lru

lru和redis的内存回收算法是有关系的,如果是redis打开了maxmemory选项,那么内存回收算法选择的是volatile-lru或allkeys-lru,这两种算法,每当redis的内存超过maxmemory的时候,redis就会优先选择lru的空转时间最长的对象进行释放

还有一个refcount

这个东西是和共享对象有关的,新对象创建的时候,初始话为1,之后如果有新程序使用了该对象,这个refcount+1没使用了就-1,如果refcount=0的时候,说明对象占用的内存会被释放

redis目前的共享对象支持的只有整数值的字符串对象,但是其他八种都可能使用共享对象

这里要注意,本篇文章是redis3.0那个时候还是五种,这里的八种我认为是可以延伸的,如果有不正确的,请指正

哈希,列表可以使用这种整数值的字符串对象

一般来说会初始化10000个字符串对象,分别是0~9999,如果你使用的话,就是使用共享对象了

最新版好像是一个好大好大的值

这一万个数字是可以进行调整的,可以自己百度下自己的版本怎么调整

之后就只剩下一个ptr了

ptr就是指向具体的数据了,这里可以认为指向的就是SDS结构,这个ptr的大小个系统有关

redis的所占大小就可以算了type是4个比特,encoding也是4个比特,然后就是lru,这个就看你的版本了,4.0是24比特,3.0是22比特,然后就是refcount一个int,4个字节,然后就剩下一个ptr指针了,现在都是64位的系统,所以都是8个字节,然后算一算就是16字节

接下来就是SDS结构了

SDS是简单动态字符串(Simple Dynamic String)

struct sdshdr {
    int len;
    int free;
    char buf[];
};

buf数组用来存放字符串的,len就是已经使用的长度,free就是还没有使用的长度

buf数组的长度就可以通过len+buf+1应为字符串都是末尾来个空字符'\0'来表示结束的

sds的优点为,可以在O(1)的情况下查找长度,获取字符串长度是O(1)的,这里我就想到了go语言中的切片slice了,里面也存在着cap最大容量上限

然后再缓冲区溢出方面,直接使用C字符串的api会导致溢出,如果字符串的长度增加的话,你没有分配内存就会导致缓冲区的溢出,这里的sds记录了长度,在对应的api造成缓冲区溢出的时候,可以自动分配内存,防止了缓冲区的溢出

在修改字符串内存方面的重分配,如果是C字符串的话,需要重新分配,也就是释放再重新申请,如果,字符串长度增大会导致内存缓冲区溢出,字符串长度减少时会造成内存泄漏。对于sds来说,应为有了len 和free,空间预分配之后,字符串长度增大在重新分配的概率就小很多,应为一般我们都会分配多一点内存给他们,在内存释放方面,对于值进行修改之后,直接修改len和free就可以了,所以也很方便

对于二进制数据来说,C字符串不可以存,但是sds是可以进行存储的,应为二进制数据中可能有'\0'这种,但是sds中有len这个长度,所以可以规避这个错误

如果sds里面存储的是文本数据时可以使用C字符串的函数,但是二进制数据不行,应为这个可能是以'\0'数据也可能在数据里面,会出错

在打印日志以及不更改字符串的情况下,才会使用C字符串,不然一般都是使用sds结构体

转换过程是不可逆的

只能是小内存编码向大内存编码进行转换


网站公告

今日签到

点亮在社区的每一天
去签到