redis(7)-内存结构

本文深入探讨Redis的内存消耗,包括对象内存、缓冲内存、内存碎片和子进程内存消耗。介绍了如何通过内存使用统计、数据对齐、安全重启等手段来管理内存,并讨论了内存碎片的产生和控制。此外,还涵盖了Redis的内存回收策略,如过期键对象的删除和内存溢出控制策略。最后,提出了内存优化技巧,如缩减键值对象、共享对象池的使用和字符串优化,以提升Redis内存使用效率。

如何高效利用Redis内存变得非常重要。 高效利用Redis内存首先需要理解Redis内存消耗在哪里, 如何管理内存, 最后才能考虑如何优化内存。 掌握这些知识后能够实现用更少的内存存储更多的数据, 从而降低成本。
·内存消耗分析。
·管理内存的原理与方法。
·内存优化技巧。
内存消耗
理解Redis内存, 首先需要掌握Redis内存消耗在哪些方面。 有些内存消耗是必不可少的, 而有些可以通过参数调整和合理使用来规避内存浪费。 内存消耗可以分为进程自身消耗和子进程消耗。

内存使用统计
首先需要了解Redis自身使用内存的统计数据, 可通过执行info memory命令获取内存相关指标。 读懂每个指标有助于分析Redis内存使用情况。
在这里插入图片描述需要重点关注的指标有: used_memory_rss和used_memory以及它们的比值mem_fragmentation_ratio。
当mem_fragmentation_ratio>1时, 说明used_memory_rss-used_memory多出的部分内存并没有用于数据存储, 而是被内存碎片所消耗, 如果两者相差很大, 说明碎片率严重。
当mem_fragmentation_ratio<1时, 这种情况一般出现在操作系统把Redis内存交换(Swap) 到硬盘导致, 出现这种情况时要格外关注, 由于硬盘速度远远慢于内存, Redis性能会变得很差, 甚至僵死。

内存消耗划分
Redis进程内消耗主要包括: 自身内存+对象内存+缓冲内存+内存碎片,其中Redis空进程自身内存消耗非常少, 通常used_memory_rss在3MB左右,used_memory在800KB左右, 一个空的Redis进程消耗内存可以忽略不计。
在这里插入图片描述
下面介绍另外三种内存消耗。
1.对象内存
对象内存是Redis内存占用最大的一块, 存储着用户所有的数据。 Redis所有的数据都采用key-value数据类型, 每次创建键值对时, 至少创建两个类型对象: key对象和value对象。 对象内存消耗可以简单理解为sizeof(keys)+sizeof(values) 。 键对象都是字符串, 在使用Redis时很容易忽略键对内存消耗的影响, 应当避免使用过长的键。 value对象更复杂些, 主要包含5种基本数据类型: 字符串、 列表、 哈希、 集合、 有序集合。 其他数据类型都是建立在这5种数据结构之上实现的, 如: Bitmaps和HyperLogLog使用字符串实现, GEO使用有序集合实现等。 每种value对象类型根据使用规模不同, 占用内存不同。 在使用时一定要合理预估并监控value对象占用情况, 避免内存溢出。
2.缓冲内存
缓冲内存主要包括: 客户端缓冲、 复制积压缓冲区、 AOF缓冲区。

客户端缓冲指的是所有接入到Redis服务器TCP连接的输入输出缓冲。输入缓冲无法控制, 最大空间为1G, 如果超过将断开连接。 输出缓冲通过参数client-output-buffer-limit控制。

复制积压缓冲区: Redis在2.8版本之后提供了一个可重用的固定大小缓冲区用于实现部分复制功能, 根据repl-backlog-size参数控制, 默认1MB。 对于复制积压缓冲区整个主节点只有一个, 所有的从节点共享此缓冲区, 因此可以设置较大的缓冲区空间, 如100MB, 这部分内存投入是有价值的, 可以有效避免全量复制。

AOF缓冲区: 这部分空间用于在Redis重写期间保存最近的写入命令。 AOF缓冲区空间消耗用户无法控制, 消耗的内存取决于AOF重写时间和写入命令量, 这部分空间占用通常很小。

3.内存碎片
Redis默认的内存分配器采用jemalloc, 可选的分配器还有: glibc、tcmalloc。 内存分配器为了更好地管理和重复利用内存, 分配内存策略一般采用固定范围的内存块进行分配。 例如jemalloc在64位系统中将内存空间划分为: 小、 大、 巨大三个范围。 每个范围内又划分为多个小的内存块单位。

比如当保存5KB对象时jemalloc可能会采用8KB的块存储, 而剩下的3KB空间变为了内存碎片不能再分配给其他对象存储。 内存碎片问题虽然是所有内存服务的通病, 但是jemalloc针对碎片化问题专门做了优化, 一般不会存在过度碎片化的问题, 正常的碎片率(mem_fragmentation_ratio) 在1.03左右。

·数据对齐: 在条件允许的情况下尽量做数据对齐, 比如数据尽量采用数字类型或者固定长度字符串等, 但是这要视具体的业务而定, 有些场景无法做到。

·安全重启: 重启节点可以做到内存碎片重新整理, 因此可以利用高可用架构, 如Sentinel或Cluster, 将碎片率过高的主节点转换为从节点, 进行安全重启。

子进程内存消耗
子进程内存消耗主要指执行AOF/RDB重写时Redis创建的子进程内存消耗。 Redis执行fork操作产生的子进程内存占用量对外表现为与父进程相同,理论上需要一倍的物理内存来完成重写操作。

子进程内存消耗总结如下:
·Redis产生的子进程并不需要消耗1倍的父进程内存, 实际消耗根据期间写入命令量决定, 但是依然要预留出一些内存防止溢出。
·需要设置sysctl vm.overcommit_memory=1允许内核可以分配所有的物理内存, 防止Redis进程执行fork时因系统剩余内存不足而失败。
·排查当前系统是否支持并开启THP, 如果开启建议关闭, 防止copy-onwrite期间内存过度消耗。

内存管理
Redis主要通过控制内存上限和回收策略实现内存管理。

1.设置内存上限
Redis使用maxmemory参数限制最大可用内存。 限制内存的目的主要有:
·用于缓存场景, 当超出内存上限maxmemory时使用LRU等删除策略释放空间。
·防止所用内存超过服务器物理内存。

2.动态调整内存上限
Redis的内存上限可以通过config set maxmemory进行动态修改, 即修改最大可用内存。 通过动态修改maxmemory, 可以实现在当前服务器下动态伸缩Redis内存的目的。

3 内存回收策略
Redis的内存回收机制主要体现在以下两个方面:
·删除到达过期时间的键对象。
·内存使用达到maxmemory上限时触发内存溢出控制策略。

3.1 删除过期键对象
Redis所有的键都可以设置过期属性, 内部保存在过期字典中。 由于进程内保存大量的键, 维护每个键精准的过期删除机制会导致消耗大量的CPU, 对于单线程的Redis来说成本过高, 因此Redis采用惰性删除和定时任务删除机制实现过期键的内存回收。

·惰性删除: 惰性删除用于当客户端读取带有超时属性的键时, 如果已经超过键设置的过期时间, 会执行删除操作并返回空, 这种策略是出于节省CPU成本考虑, 不需要单独维护TTL链表来处理过期键的删除。 但是单独用这种方式存在内存泄露的问题, 当过期键一直没有访问将无法得到及时删
除, 从而导致内存不能及时释放。 正因为如此, Redis还提供另一种定时任务删除机制作为惰性删除的补充。

·定时任务删除: Redis内部维护一个定时任务, 默认每秒运行10次(通过配置hz控制) 。 定时任务中删除过期键逻辑采用了自适应算法, 根据键的过期比例、 使用快慢两种速率模式回收键。

流程说明:
1) 定时任务在每个数据库空间随机检查20个键, 当发现过期时删除对应的键。
2) 如果超过检查数25%的键过期, 循环执行回收逻辑直到不足25%或运行超时为止, 慢模式下超时时间为25毫秒。
3) 如果之前回收键逻辑超时, 则在Redis触发内部事件之前再次以快模式运行回收过期键任务, 快模式下超时时间为1毫秒且2秒内只能运行1次。
4) 快慢两种模式内部删除逻辑相同, 只是执行的超时时间不同。

3.2.内存溢出控制策略

当Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略。具体策略受maxmemory-policy参数控制, Redis支持6种策略, 如下所示:
1) noeviction: 默认策略, 不会删除任何数据, 拒绝所有写入操作并返回客户端错误信息(error) OOM command not allowed when used memory, 此时Redis只响应读操作。
2) volatile-lru: 根据LRU算法删除设置了超时属性(expire) 的键, 直到腾出足够空间为止。 如果没有可删除的键对象, 回退到noeviction策略。
3) allkeys-lru: 根据LRU算法删除键, 不管数据有没有设置超时属性,直到腾出足够空间为止。
4) allkeys-random: 随机删除所有键, 直到腾出足够空间为止。
5) volatile-random: 随机删除过期键, 直到腾出足够空间为止。
6) volatile-ttl: 根据键值对象的ttl属性, 删除最近将要过期数据。 如果没有, 回退到noeviction策略。

内存优化

Redis所有的数据都在内存中, 而内存又是非常宝贵的资源。 如何优化内存的使用一直是Redis用户非常关注的问题。 本节深入到Redis细节中, 探索内存优化的技巧。

Redis存储的所有值对象在内部定义为redisObject结构体, 内部结构如图:
在这里插入图片描述
Redis存储的数据都使用redisObject来封装, 包括string、 hash、 list、set、 zset在内的所有数据类型。 理解redisObject对内存优化非常有帮助, 下面针对每个字段做详细说明:

·type字段: 表示当前对象使用的数据类型, Redis主要支持5种数据类型: string、 hash、 list、 set、 zset。 可以使用type{key}命令查看对象所属类型, type命令返回的是值对象类型, 键都是string类型。

·encoding字段: 表示Redis内部编码类型, encoding在Redis内部使用,代表当前对象内部采用哪种数据结构实现。 理解Redis内部编码方式对于优化内存非常重要, 同一个对象采用不同的编码实现内存占用存在明显差异。

·lru字段: 记录对象最后一次被访问的时间, 当配置了maxmemory和maxmemory-policy=volatile-lru或者allkeys-lru时, 用于辅助LRU算法删除键数据。 可以使用object idletime{key}命令在不更新lru字段情况下查看当前键的空闲时间。

·refcount字段: 记录当前对象被引用的次数, 用于通过引用次数回收内存, 当refcount=0时, 可以安全回收当前对象空间。 使用object refcount{key}获取当前对象引用。 当对象为整数且范围在[0-9999]时, Redis可以使用共享对象的方式来节省内存。 具体细节见之后“共享对象池”部分。

·*ptr字段: 与对象的数据内容相关, 如果是整数, 直接存储数据; 否则表示指向数据的指针。 Redis在3.0之后对值对象是字符串且长度<=39字节的数据, 内部编码为embstr类型, 字符串sds和redisObject一起分配, 从而只要一次内存操作即可。

缩减键值对象

降低Redis内存使用最直接的方式就是缩减键(key) 和值(value) 的长度。
·key长度: 如在设计键时, 在完整描述业务情况下, 键值越短越好。 如user: {uid}: friends: notify: {fid}可以简化为u: {uid}: fs: nt: {fid}。
·value长度: 值对象缩减比较复杂, 常见需求是把业务对象序列化成二进制数组放入Redis。 首先应该在业务上精简业务对象, 去掉不必要的属性避免存储无效数据。 其次在序列化工具选择上, 应该选择更高效的序列化工具来降低字节数组大小。 以Java为例, 内置的序列化方式无论从速度还是压
缩比都不尽如人意, 这时可以选择更高效的序列化工具, 如: protostuff、kryo等。

值对象除了存储二进制数据之外, 通常还会使用通用格式存储数据比如: json、 xml等作为字符串存储在Redis中。 这种方式优点是方便调试和跨语言, 但是同样的数据相比字节数组所需的空间更大, 在内存紧张的情况下, 可以使用通用压缩算法压缩json、 xml后再存入Redis, 从而降低内存占
用, 例如使用GZIP压缩后的json可降低约60%的空间。

共享对象池

共享对象池是指Redis内部维护[0-9999]的整数对象池。 创建大量的整数类型redisObject存在内存开销, 每个redisObject内部结构至少占16字节, 甚至超过了整数自身空间消耗。 所以Redis内存维护一个[0-9999]的整数对象池, 用于节约内存。 除了整数值对象, 其他类型如list、 hash、 set、 zset内部元素也可以使用整数对象池。 因此开发中在满足需求的前提下, 尽量使用整数对象以节省内存。
整数对象池在Redis中通过变量REDIS_SHARED_INTEGERS定义, 不能通过配置修改。可以通过object refcount命令查看对象引用数验证是否启用整数对象池技术。

使用共享对象池后, 相同的数据内存使用降低30%以上。 可见当数据大量使用[0-9999]的整数时, 共享对象池可以节约大量内存。 需要注意的是对象池并不是只要存储[0-9999]的整数就可以工作。 当设置maxmemory并启用LRU相关淘汰策略如: volatile-lru, allkeys-lru时, Redis禁止使用共享对象池。

为什么开启maxmemory和LRU淘汰策略后对象池无效?
LRU算法需要获取对象最后被访问时间, 以便淘汰最长未访问数据, 每个对象最后访问时间存储在redisObject对象的lru字段。 对象共享意味着多个引用共享同一个redisObject, 这时lru字段也会被共享, 导致无法获取每个对象的最后访问时间。 如果没有设置maxmemory, 直到内存被用尽Redis也不会触发内存回收, 所以共享对象池可以正常工作。
综上所述, 共享对象池与maxmemory+LRU策略冲突, 使用时需要注意。 对于ziplist编码的值对象, 即使内部数据为整数也无法使用共享对象池, 因为ziplist使用压缩且内存连续的结构, 对象共享判断成本过高。

为什么只有整数对象池?
首先整数对象池复用的几率最大, 其次对象共享的一个关键操作就是判断相等性, Redis之所以只有整数对象池, 是因为整数比较算法时间复杂度为O(1) , 只保留一万个整数为了防止对象池浪费。 如果是字符串判断相等性, 时间复杂度变为O(n) , 特别是长字符串更消耗性能(浮点数在
Redis内部使用字符串存储) 。 对于更复杂的数据结构如hash、 list等, 相等性判断需要O(n2) 。 对于单线程的Redis来说, 这样的开销显然不合理, 因此Redis只保留整数共享对象池。

字符串优化
字符串对象是Redis内部最常用的数据类型。 所有的键都是字符串类型, 值对象数据除了整数之外都使用字符串存储。 比如执行命令: lpush cache: type"redis"“memcache”“tair”“levelDB”, Redis首先创建"cache: type"键字符串, 然后创建链表对象, 链表对象内再包含四个字符串对象, 排除
Redis内部用到的字符串对象之外至少创建5个字符串对象。 可见字符串对象在Redis内部使用非常广泛, 因此深刻理解Redis字符串对于内存优化非常有帮助。

1.字符串结构
Redis没有采用原生C语言的字符串类型而是自己实现了字符串结构, 内部简单动态字符串。
在这里插入图片描述
Redis自身实现的字符串结构有如下特点:
·O(1) 时间复杂度获取: 字符串长度、 已用长度、 未用长度。
·可用于保存字节数组, 支持安全的二进制数据存储。
·内部实现空间预分配机制, 降低内存再分配次数。
·惰性删除机制, 字符串缩减后的空间不释放, 作为预分配空间保留。

2.预分配机制
因为字符串(SDS) 存在预分配机制, 日常开发中要小心预分配带来的内存浪费。

3.字符串重构
字符串重构: 指不一定把每份数据作为字符串整体存储, 像json这样的数据可以使用hash结构, 使用二级结构存储也能帮我们节省内存。 同时可以使用hmget、 hmset命令支持字段的部分读取修改, 而不用每次整体存取。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值