参考自《redis开发与运维》

1. 内存消耗

1.1 内存使用统计

    info memory指令,重点内容如下:

    used_memory:redis内部数据所占内存总量

    used_memory_rss:从操作系统角度看redis占用的内存总量

    used_memory_peak:used_memory的峰值

    mem_fragmentation_ratio:内存碎片率,used_memory_rss/info memory

    如果内存碎片率大于1很多,则说明碎片化严重,如果小于1表示redis把内存里的数据交换到了硬盘,需要特别注意这个参数。

1.2 内存组成

    (1)自身内存:很小,可忽略不计

    (2)对象内存:包括redis中所有的key和对应的值所占的内存空间,因此应当避免过长的键,而值需要根据具体类型选择相应的编码。

    (3)缓冲内存:

        客户端缓冲:

        输入输出缓冲区,输入缓冲区不可控,最大1G,输出缓冲区可以通过client-output-buffer-limit 参数控制。

        普通客户端输出缓冲区:redis默认是不限制,一般普通客户端的内存消耗可以忽略不计,但某些指令如monitor就需要特别关注。

        从客户端输出缓冲区:默认为client-output-buffer-limit slave256mb64mb60,当主从节点网络不好(如跨机房)或者主节点挂载多个从节点时,可能导致输出缓冲区过大。

        订阅客户端输出缓冲区:默认为client-output-buffer-limit pubsub32mb8mb60,当消息生产者消息生成过快而消费速度过慢时,也有可能造成输出缓冲区溢出

        复制积压缓冲区:redis2.8后才有,在主节点中存在,和所有从节点共享,用于实现部分复制功能,可以通过参数repl-backlog-size适当调大(如100M),它可以有效避免全量复制,默认为1M。

        AOF缓冲区:用于redis在使用AOF方式时持久化数据到磁盘,无法控制,内存消耗取决于AOF持久时间和写入命令量。一般较小。

    (4)内存碎片:根redis内存分配有关

        redis默认使用jemalloc进行内存分配。它将内存空间划分为小、中、大三个范围,每个范围又划分为多个小的内存块单位,比如保存5k对象时,需要用8k的块进行存储,一定程度上会出现内存碎片。内存碎片率正常在1.03左右。

    内存碎片出现场景:频繁做更新操作,如对已经存在的键做append、setrange操作;大量过期键删除,释放的空间不能有效利用时。

    内存碎片解决方案:安全重启(单实例则直接重启服务,有主从集群等关系就切换节点为从节点然后重启服务)和数据对齐(数据尽量采用固定长度字符串或数字类型)

     

redis占用内存 redis内存消耗_redis占用内存

 

 

 

1.3 子进程内存消耗

    当redis在执行AOF或RDB持久化时,需要fork一个子进程来进行持久化,而子进程的内存消耗和父进程相同,因此Linux出现了写时复制的技术,即父进程只有在写的时候才会复制相关页,而子进程读取父进程的内存进行持久化,即子进程读取的是fork时的父进程内存快照。

    THP问题:即父进程在复制相关内存时由原来的4k(内存页单位)变成2M,如果父进程有大量写,会加重内存拷贝,从而造成过度内存消耗。因此需要关闭THP功能。

2. 内存管理

2.1 设置内存上限

    maxmemory限制最大可用内存,可用防止redis的内存超过物理内存。

    注意:maxmemory是redis实际使用的内存量,也就是used_memory统计的内存,即对象内存区域,不包括缓冲区、内存碎片,因此需要注意这部分内存溢出。

2.2 动态调整内存上限

    通过config set maxmemory进行动态修改最大可用内存。

    2.3 内存回收策略

2.3.1 删除过期键对象

    redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定时遍历这个字典来删除到期的 key。

    惰性删除,当客户端读取带有超时属性的键时,如果该键已经过时,那么删除该键值,并返回空。如果过期键一直没有读取,则不会及时释放。因此有了定时任务删除模式。

    定期任务删除: Redis 默认会每秒进行10次(通过hz控制)过期扫描,过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略。

        从过期字典中随机 20 个 key;
        删除这 20 个 key 中已经过期的 key;
        如果过期的 key 比率超过 1/4,那就重复步骤 1;

 该策略中采用了自适应快慢两种算法运行模式来回收键,流程如下:

 1)定时任务在每个数据库空间随机检查20个键,发现过期时删除对应的键。

 2)如果检索出来的键超过25%都是过期的键,那么将会循环执行回收逻辑操作 直到比例不足 25% 或运行到超时为止(为了避免过期扫描或者循环回收过度导致卡死,慢模式回收默认是25毫秒超时)

 3)如果之前的回收键逻辑超时,则在 Redis出发内部事件之前再次以快模式运行回收过期任务,快模式下超时时间为1毫秒,且 2 秒内只能运行一次。

 4)快慢模式的内部删除回收逻辑相同,只是超时事件不同罢了。

    如果某一时刻,有大量key同时过期,Redis 会持续扫描过期字典,造成客户端响应卡顿,因此设置过期时间时,就尽量避免这个问题,在设置过期时间时,可以给过期时间设置一个随机范围,避免同一时刻过期。

2.3.2 内存溢出控制策略

    (1)noeviction:默认策略,不删除数据,也拒绝写入,即redis变成只读操作。

    (2)volatile-lru:根据LRU算法删除设置了超时属性的键,直到有足够的空间为止。如果没有可删除对象,回退到默认策略。

    (3)allkeys-lru:根据lru算法删除键,不管数据是否设置了超时属性,直到腾出足够空间为止

    (4)allkeys-random:随机删除所有键,直到腾出足够空间为止。

    (5)volatile-random:随机删除过期键,直到腾出足够空间为止。

    (6)volatile-ttl:根据键值对象的ttl属性删除最近将要过期数据,如果没有回退到默认策略

    内存溢出控制策略可以采用config set maxmemory-policy{policy}动态配置。当Redis一直工作在内存溢出(used_memory>maxmemory)的状态下且设置非noeviction策略时,会频繁地触发回收内存的操作,影响Redis的性能。建议线上Redis内存工作在maxmemory>used_memory状态下,避免频繁内存回收开销。

3.内存优化

3.1 redisObject对象

Redis存储的所有值对象在内部定义为redisObject结构体,如下图所示:

   

redis占用内存 redis内存消耗_数据_02

 

 

 

    高并发写入场景中,在条件允许的情况下,建议字符串长度控制在39字节以内,减少创建redisObject内存分配次数,从而提高性能。

3.2 缩减键值对象

    降低Redis内存使用最直接的方式就是缩减键(key)和值(value)的长度。

    ·key长度:如在设计键时,在完整描述业务情况下,键值越短越好。

    ·value长度:值对象缩减比较复杂,一般为二进制数组或格式化存储(如json)。

    在业务上精简对象,去掉不必要的属性

    二进制数组在序列化时,选择更高效的序列化工具来降低字节数组大小

    通用格式存储数据时,应考虑压缩速度和计算成本,如Google的Snappy压缩工具,降低存储空间。

3.3 共享对象池

    共享对象池是指Redis内部维护[0-9999]的整数对象池。创建大量的整数类型redisObject存在内存开销,每个redisObject内部结构至少占16字节,甚至超过了整数自身空间消耗。所以Redis内存维护一个[0-9999]的整数对象池,用于节约内存。除了整数值对象,其他类型如list、hash、set、zset内部元素也可以使用整数对象池。因此开发中在满足需求的前提下,尽量使用整数对象以节省内存。

    可以通过object refcount key 命令查看对象引用数。假设set foo 100,set bar 100这2个指令执行完后,这内存变成如下图所示:

     

redis占用内存 redis内存消耗_数据_03

 

 

    注意:以下情况无法使用共享对象

    内存回收策略为maxmemory+LRU时

    值对象编码为ziplist时,因为ziplist使用压缩且内存连续的结构,对象共享判断成本过高

3.4 字符串优化

    redis没有采用原生C的字符串类型,而是进行了封装,包括字符串已用长度、空闲长度、字符数组3个部分组成。内部实现空间预分配机制。第一次set时,正常分配,第二次使用append或setrange时,会进行空间的动态扩展,类似Java的ArrayList动态扩容机制,造成内存空间的浪费和内存碎片化加剧。因此在开发时,不要使用append或setrange指令。直接使用 set 

    字符串重构,对于json作为值时,可以使用hash结构进行重构优化。这样能提高内存使用率,可以使用hmget和hmset读取和修改部分字段,降低网络开销。

3.5 编码优化

    在redis中,每种类型都对应一个或多个底层数据结构,我们称之为值的编码,编码不同将直接影响数据的内存占用和读写效率。可通过object encoding key来查询。

    编码类型在redis写入数据时自动完成,这个过程是不可逆的,转化规则只能从小内存编码向大内存编码转换。

   

redis占用内存 redis内存消耗_redis_04

 

 

    3.5.1 ziplist编码

    ziplist编码主要目的是为了节约内存,因此所有数据都是采用线性连续的内存结构。ziplist编码是应用范围最广的一种,可以分别作为hash、list、zset类型的底层数据结构实现。

    

redis占用内存 redis内存消耗_redis占用内存_05

 

 

    ziplist压缩编码的性能表现跟值长度和元素个数密切相关,正因为如此Redis提供了{type}-max-ziplist-value和{type}-max-ziplist-entries相关参数来做控制ziplist编码转换。最后再次强调使用ziplist压缩编码的原则:追求空间和时间的平衡,即减少内存空间的占用,但读写性能会降低。

    针对性能要求较高的场景使用ziplist,建议长度不要超过1000,每个元素大小控制在512字节以内。

    命令平均耗时使用info Commandstats命令获取,包含每个命令调用次数、总耗时、平均耗时,单位为微秒。

 3.6 控制键的数量

    当使用Redis存储大量数据时,通常会存在大量键,过多的键同样会消耗大量内存。Redis本质是一个数据结构服务器,它为我们提供多种数据结构,如hash、list、set、zset等。使用Redis时不要进入一个误区,大量使用get/set这样的API,把Redis当成Memcached使用。对于存储相同的数据内容利用Redis的数据结构降低外层键的数量,也可以节省大量内存。

    使用hash重构后节省内存量效果非常明显,特别对于存储小对象的场景,下面分析这种内存优化技巧的关键点:

    (1)hash类型节省内存的原理是使用ziplist编码,如果使用hashtable编码方式反而会增加内存消耗。

    (2)ziplist长度需要控制在1000以内,否则读写长列表会导致CPU消耗严重,得不偿失。

    (3)ziplist适合存储小对象,对于大对象不但内存优化效果不明显还会增加命令操作耗时。

    (4)需要预估键的规模,从而确定每个hash结构需要存储的元素数量。

    (5)根据hash长度和元素大小,调整hash-max-ziplist-entries和hash-max-ziplist-value参数,确保hash类型使用ziplist编码。

    缺点:

        hash重构后所有的键无法再使用超时(expire)和LRU淘汰机制自动删除,需要手动维护删除。

        对于大对象,如1KB以上的对象,使用hash-ziplist结构控制键数量反而得不偿失。

    优点:

        对于大量小对象的存储场景,非常适合使用ziplist编码的hash类型控制键的规模来降低内存。