Redis所有的数据都存在内存中,当前内存虽然越来越便宜,但跟廉价的硬盘相比成本还是比较昂贵,因此如何高效利用Redis内存变得非常重要。高效利用Redis内存首先需要理解Redis内存消耗在哪里,如何管理内存,最后才能考虑如何优化内存。掌握这些知识后能够实现用更少的内存存储更多的数据,从而降低成本。
内存消耗
如何查看Redis中内存的消耗情况哪?可以通过 info命令,查看Redis内存消耗的相关指标,从而有助于更好的分析内存。执行命令之后有这么几个重要的指标:
重点需要关注下mem_fragmentation_ratio这个值:
mem_fragmentation_ratio > 1 说明多出来的部分名没有用于数据存储,而是被内存碎片所消耗,相差越大,说明内存碎片率越严重。
mem_fragmentation_ratio < 1 一般出现在Redis内存交换(Swap)到硬盘导致(used_memory > 可用最大内存时,Redis会把旧的和不适用的数据写入到硬盘,这块空间就叫Swap空间),出现这种情况需要格外关注,硬盘速度远远慢于内存,Redis性能就会变得很差,甚至僵死。
1.1、内存消耗的划分
Redis的内存主要包括:对象内存+缓冲内存+自身内存+内存碎片。
1、对象内存 对象内存是Redis内存中占用最大一块,存储着所有的用户的数据。Redis所有的数据都采用key-value数据类型,每次创建键值对时,至少创建两个类型的变量:key对象和value对象。对象内存消耗可以简单理解为sizeof(keys)+sizeof(values)。key对象都是字符串,在使用Redis时很容易忽略键值对内存消耗影响,应该避免使用过长的键。value对象的存储方式,五种数据类型–String,List,Hash,Set,Zset。每种存储方式在使用的时候长度、数据类型不同,则占用的内存就不同。
2、缓冲内存 主要包括:客户端缓冲、复制积压缓冲区、AOF缓冲区 客户端缓冲:普通的客户端的连接(大量连接),从客户端(主要是复制的时候,异地跨机房,或者主节点下有多个从节点),订阅客户端(发布订阅功能,生产大于消费就会造成积压) 复制积压缓冲:2.8版本之后提供的可重用的固定大小缓冲区用于实现部分复制功能,默认1MB,主要是在主从同步时用到。 AOF缓冲区:持久化用的,会先写入到缓冲区,然后根据响应的策略向磁盘进行同步,消耗的内存取决于写入的命令量和重写时间,通常很小。
3、内存碎片 目前可选的分配器有jemalloc、glibc、tcmalloc 默认jemalloc,内存分配器为了更好地管理和重复利用内存,分配内存策略一般采用固定范围的内存块进行分配。 出现高内存碎片问题的情况:大量的更新操作,比如append、setrange;大量的过期键删除,键对象过期删除后,释放的空间无法得到有效利用,导致碎片率上升。 解决办法:
- 数据对齐
- 安全重启(高可用/主从切换)。
4、自身内存 主要指AOF/RDB重写时Redis创建的子进程内存的消耗,Linux具有写时复制技术(copy-on-write),父子进程会共享相同的物理内存页,当父进程写请求时会对需要修改的页复制出一份副本来完成写操作。
管理内存
设置上限
Redis默认是无限使用内存。所以在使用的时候尽量的去配置maxmemory,给Redis设置内存使用上限,防止因Redis的无限使用造成系统内存耗尽。有一点需要注意的是maxmemory配置的是Redis实际使用的内存量,即used_memory,由于有内存碎片的存在,所以实际的内存使用比used_memory要大。
Redis可以动态的执行内存的调整:
config set maxmemory 6GB
配置内存回收策略
Redis的内存回收机制主要体现在两个方面上:
-
对过期数据的处理
-
当内存使用情况达到maxmemory时触发内存回收策略
- 过期键的删除
惰性删除:什么时候执行呢?就是在客户端读取带有超时属性的键时,如果已经超过键值设置的过期时间,则删除并返回空。这样做的目的主要是为了节省CPU成本考虑,不需要单独维护TTL链表来处理过期键的删除。但是,如果单独使用这种方式存在一个问题,如果当前的键值永远不再被访问呢?就不删除了吗?那肯定不行,这就会造成内存泄漏的问题。那Redis是怎么解决的呢?Redis提供了一个定时任务的删除机制来做补充。
- 定时任务删除
Redis内部维护了一个定时任务,默认是每秒运行十次。删除的逻辑如下图:
内存溢出控制策略
当Redis使用的内存达到上限maxmemory后,就会根据maxmemory-policy设置的相关策略进行对应的操作,Redis支持一下6中策略:
3、内存优化
Hashtable
Redis所有的数据存储都是Key-Value的数据类型。整体的结构是Redis自己实现的hashtable,Redis有两个hashtable存在,但是只有其中一个是用来存数据的,另一个hashtable的存在是为了在扩容的时候用的。通过采用渐进式的方式,把旧的hashtable中的数据逐渐的复制到另外一个hashtable中去。为什么采用渐进式呢?因为Redis是单线程的,扩容一直数据的迁移是很耗费时间的,所以迁移的过程是不能对Redis的其他使用造成影响。所以采用渐进式。
因此,这个hashtable的结构就变得很重要了,hashtable的设计时数组加链表的方式实现,一维是数组结构,二维是一个链表结构,在一维数组中存的是指向链表中第一条数据的指针。
//数组结构
struct dictht {
dictEntry** table; // 二维
long size; // 第一维数组的长度
long used; // hash 表中的元素个数
}
//字典实体
struct dictEntry {
void* key;
void* val;
dictEntry* next; // 链接下一个 entry
}
redisObect对象
Redis中所有的值对象内部定义都是redisObject结构体。结构如下图:
struct RedisObject {
int4 type; // 4bits
int4 encoding; // 4bits
int24 lru; // 24bits
int32 refcount; // 4bytes
void *ptr; // 8bytes,64-bit system
} robj;...