「Redis」内存管理机制

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: Redis内存管理机制

基于Redis-3.2.1


简介


Redis是一个基于内存的键值对的数据库,其内存管理是非常重要的。下面从Redis的内存消耗、内存管理、内存优化三个角度对Redis的内存机制进行剖析和学习。


内存消耗


内存查看


127.0.0.1:6379> info memory
通过Redis客户端命令info memory查看内存情况

参数

含义

used_memory

Redis内部存储所用的内存使用量

used_memory_human

以可读形式返回 used_memory

used_memory_rss

以系统角度返回Redis进程的内存使用量,Redis向系统申请的内存量

used_memory_rss_human

以可读形式返回 used_memory_rss

used_memory_peak

内存使用的峰值,used_memory的峰值

used_memory_peak_human

以可读形式返回used_memory_peak

mem_fragmentation_ratio

内存碎片率,used_memory_rss / used_memory

mem_allocator

Redis使用的内存分配器。默认是jemalloc


下面是一个本机命令输出情况:


127.0.0.1:6379> info memory

used_memory:709320

used_memory_human:692.70K

used_memory_rss:672384

used_memory_rss_human:656.63K

used_memory_peak:787384

used_memory_peak_human:768.93K

total_system_memory:0

total_system_memory_human:0B

used_memory_lua:37888

used_memory_lua_human:37.00K

maxmemory:0

maxmemory_human:0B

maxmemory_policy:noeviction

mem_fragmentation_ratio:0.95

mem_allocator:jemalloc-3.6.0


内存碎片


内存碎片率通过mem_fragmentation_ratio = used_memory_rss / used_memory计算获得

内存碎片率

含义

措施

>1

说明used_memory_rss-used_memory多出 的部分内存并没有用于数据存储,而是被内存碎片所消耗,如果两者相差很 大,说明碎片率严重

及时进行碎片清理,一般1~1.5

属于正常范围

<1

这种情况一般出现在操作系统把Redis 内存交换(Swap)到硬盘导致,出现这种情况时要格外关注,由于硬盘速度远远慢于内存,Redis性能会变得很差,甚至僵死

已经开始使用硬盘进行存储,需要考虑扩容内存


为何会有碎片问题产生?


  • Redis内部有自己的内存管理器,为了提高内存使用的效率,来对内存的申请和释放进行管理。
  • Redis中的值删除的时候,并没有把内存直接释放,交还给操作系统,而是交给了Redis内部有内存管理器。
  • Redis中申请内存的时候,也是先看自己的内存管理器中是否有足够的内存可用。
  • Redis的这种机制,提高了内存的使用率,但是会使Redis中有部分自己没在用,却不释放的内存,导致了内存碎片的发生。


内存构成


网络异常,图片无法展示
|

Redis的内存主要由 自身内存、对象内存、缓冲内存、内存碎片 几部分构成。


  • 自身内存 自身进程内存占用很小,可以忽略不计
  • 对象内存
    对象内存是Redis中内存消耗的主要部分,所有用户数据都存储在对象内存中,Redis支持字符串、列表、哈希、集合、有序集合五种数据类型,不同数据类型又由不同的数据结构提供底层实现的,可以参考 [Redis]数据结构与对象
  • 缓冲内存
    缓冲缓存主要包括客户端缓冲、复制积压缓冲区、AOF缓冲区
  • 内存碎片
    Redis中默认的内存分配器是jemalloc,其他内存分配器还有glibc、tcmalloc
    内存管理器一般会以块形式对内存进行管理和空间分配。首先按照不同的大小范围进行范围内块大小的定义,根据实际对象的大小来进行内存空间的分配
    网络异常,图片无法展示
    |


内存碎片产生原因


网络异常,图片无法展示
|


  • 数据不对齐 由于以块形式存储,内存块空间大小是固定标准,当对象与块大小不对齐时无法充分利用该内存空间
  • 频繁更新操作append 等频繁变更操作有可能会导致内存碎片产生
  • 大量键过期删除 当出现大量键对象 过期 后,释放的空间未充分利用会导致碎片率上升


内存碎片规避方式


  • 数据对齐 尽量使用定长数据类型,保持内存块对齐,也就是说尽可能物尽其用,用多少占用多少
  • 安全重启 在集群环境下,可以主从节点切换,通过重启服务来重载内存以提高内存使用率


子进程内存消耗


这里主要是指Redis在AOF/RDB时需要fork()创建出的子进程的内存消耗。
由于Linux中有写时复制(copy-on-write)技术来使父子进程共享物理内存页,只有当数据发生变更时才产生实际复制,因此无需预留占用和父进程一样的内存空间。


这里需要关注下Linux提供的Transport Huge Page开启状态下产生的问题,Linux kernel在2.6.38内核增加了THP特性,支持大内存页(2MB)分配,默 认开启。当开启时可以降低fork子进程的速度,但fork操作之后,每个内存 页从原来4KB变为2MB,会大幅增加重写期间父进程内存消耗。同时每次写 命令引起的复制内存页单位放大了512倍,会拖慢写操作的执行时间,导致大量写操作慢查询,例如简单的incr命令也会出现在慢查询中。因此Redis日志中建议将此特性进行禁用。


内存管理


Redis主要通过控制内存上限内存回收策略进行内存管理和控制。可通过config set maxmemory命令进行动态修改。


内存上限


通过maxmemory来设置内存使用上限,也就是限制used_memory中各项内存大小的总和,由于存在内存碎片,Redis在系统中实际使用的内存要大于maxmemory


内存回收


内存回收策略


内存回收主要在以下两个方面:


  • 删除过期的键对象
    为了避免过多消耗资源性能维护大量的过期键,采用惰性删除、定期任务进行过期键对象内存回收。
  • 惰性删除 当执行命令访问到键对象具有超时属性时,则进行移除。该方式存在的问题是若过期对象一直不被访问则永远无法回收。
  • 定期任务 通过自定义的自适应算法执行慢模式、快模式两种进行删除,间隔时间默认为10s(参数hz控制)。感兴趣可以自行查看源码剖析,不再赘述。
  • 当内存使用到达maxmemory时,触发内存回收策略对内存空间进行回收处理


内存回收策略主要有noeviction、volatile-lru、allkeys-lru、allkeys-random、volatile-random、volatile-ttl六种,可以通过config set maxmemory-policy进行动态配置控制。

回收策略

执行方式

noeviction

默认策略,不会删除任何数据,拒绝所有写入操作并返 回客户端错误信息(error)OOM command not allowed when used memory,此 时Redis只响应读操作。

volatile-lru

根据LRU算法删除设置了超时属性(expire)的键,直到腾出足够空间为止。如果没有可删除的键对象,回退到noeviction策略。

allkeys-lru

根据LRU算法删除键,不管数据有没有设置超时属性, 直到腾出足够空间为止。

allkeys-random

随机删除所有键,直到腾出足够空间为止。

volatile-random

随机删除过期键,直到腾出足够空间为止。

volatile-ttl

根据键值对象的ttl属性,删除最近将要过期数据。如果 没有,回退到noeviction策略。


内存回收源码


以下是内存回收源码


int freeMemoryIfNeeded(void) {

   size_t mem_used, mem_tofree, mem_freed;

   int slaves = listLength(server.slaves);


   /* Remove the size of slaves output buffers and AOF buffer from the

    * count of used memory. */

   // 计算出 Redis 目前占用的内存总数,但有两个方面的内存不会计算在内:

   // 1)从服务器的输出缓冲区的内存

   // 2)AOF 缓冲区的内存

   mem_used = zmalloc_used_memory();

   if (slaves) {

       listIter li;

       listNode *ln;


       listRewind(server.slaves,&li);

       while((ln = listNext(&li))) {

           redisClient *slave = listNodeValue(ln);

           unsigned long obuf_bytes = getClientOutputBufferMemoryUsage(slave);

           if (obuf_bytes > mem_used)

               mem_used = 0;

           else

               mem_used -= obuf_bytes;

       }

   }

   if (server.aof_state != REDIS_AOF_OFF) {

       mem_used -= sdslen(server.aof_buf);

       mem_used -= aofRewriteBufferSize();

   }


   /* Check if we are over the memory limit. */

   // 如果目前使用的内存大小比设置的 maxmemory 要小,那么无须执行进一步操作

   if (mem_used <= server.maxmemory) return REDIS_OK;


   // 如果占用内存比 maxmemory 要大,但是 maxmemory 策略为不淘汰,那么直接返回

   if (server.maxmemory_policy == REDIS_MAXMEMORY_NO_EVICTION)

       return REDIS_ERR; /* We need to free memory, but policy forbids. */


   /* Compute how much memory we need to free. */

   // 计算需要释放多少字节的内存

   mem_tofree = mem_used - server.maxmemory;


   // 初始化已释放内存的字节数为 0

   mem_freed = 0;


   // 根据 maxmemory 策略,

   // 遍历字典,释放内存并记录被释放内存的字节数

   while (mem_freed < mem_tofree) {

       int j, k, keys_freed = 0;


       // 遍历所有字典

       for (j = 0; j < server.dbnum; j++) {

           long bestval = 0; /* just to prevent warning */

           sds bestkey = NULL;

           dictEntry *de;

           redisDb *db = server.db+j;

           dict *dict;


           if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||

               server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM)

           {

               // 如果策略是 allkeys-lru 或者 allkeys-random

               // 那么淘汰的目标为所有数据库键

               dict = server.db[j].dict;

           } else {

               // 如果策略是 volatile-lru 、 volatile-random 或者 volatile-ttl

               // 那么淘汰的目标为带过期时间的数据库键

               dict = server.db[j].expires;

           }


           // 跳过空字典

           if (dictSize(dict) == 0) continue;


           /* volatile-random and allkeys-random policy */

           // 如果使用的是随机策略,那么从目标字典中随机选出键

           if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM ||

               server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_RANDOM)

           {

               de = dictGetRandomKey(dict);

               bestkey = dictGetKey(de);

           }


           /* volatile-lru and allkeys-lru policy */

           // 如果使用的是 LRU 策略,

           // 那么从一集 sample 键中选出 IDLE 时间最长的那个键

           else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||

               server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)

           {

               struct evictionPoolEntry *pool = db->eviction_pool;


               while(bestkey == NULL) {

                   evictionPoolPopulate(dict, db->dict, db->eviction_pool);

                   /* Go backward from best to worst element to evict. */

                   for (k = REDIS_EVICTION_POOL_SIZE-1; k >= 0; k--) {

                       if (pool[k].key == NULL) continue;

                       de = dictFind(dict,pool[k].key);


                       /* Remove the entry from the pool. */

                       sdsfree(pool[k].key);

                       /* Shift all elements on its right to left. */

                       memmove(pool+k,pool+k+1,

                           sizeof(pool[0])*(REDIS_EVICTION_POOL_SIZE-k-1));

                       /* Clear the element on the right which is empty

                        * since we shifted one position to the left.  */

                       pool[REDIS_EVICTION_POOL_SIZE-1].key = NULL;

                       pool[REDIS_EVICTION_POOL_SIZE-1].idle = 0;


                       /* If the key exists, is our pick. Otherwise it is

                        * a ghost and we need to try the next element. */

                       if (de) {

                           bestkey = dictGetKey(de);

                           break;

                       } else {

                           /* Ghost... */

                           continue;

                       }

                   }

               }

           }


           /* volatile-ttl */

           // 策略为 volatile-ttl ,从一集 sample 键中选出过期时间距离当前时间最接近的键

           else if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_TTL) {

               for (k = 0; k < server.maxmemory_samples; k++) {

                   sds thiskey;

                   long thisval;


                   de = dictGetRandomKey(dict);

                   thiskey = dictGetKey(de);

                   thisval = (long) dictGetVal(de);


                   /* Expire sooner (minor expire unix timestamp) is better

                    * candidate for deletion */

                   if (bestkey == NULL || thisval < bestval) {

                       bestkey = thiskey;

                       bestval = thisval;

                   }

               }

           }


           /* Finally remove the selected key. */

           // 删除被选中的键

           if (bestkey) {

               long long delta;


               robj *keyobj = createStringObject(bestkey,sdslen(bestkey));

               propagateExpire(db,keyobj);

               /* We compute the amount of memory freed by dbDelete() alone.

                * It is possible that actually the memory needed to propagate

                * the DEL in AOF and replication link is greater than the one

                * we are freeing removing the key, but we can't account for

                * that otherwise we would never exit the loop.

                *

                * AOF and Output buffer memory will be freed eventually so

                * we only care about memory used by the key space. */

               // 计算删除键所释放的内存数量

               delta = (long long) zmalloc_used_memory();

               dbDelete(db,keyobj);

               delta -= (long long) zmalloc_used_memory();

               mem_freed += delta;

               

               // 对淘汰键的计数器增一

               server.stat_evictedkeys++;


               notifyKeyspaceEvent(REDIS_NOTIFY_EVICTED, "evicted",

                   keyobj, db->id);

               decrRefCount(keyobj);

               keys_freed++;


               /* When the memory to free starts to be big enough, we may

                * start spending so much time here that is impossible to

                * deliver data to the slaves fast enough, so we force the

                * transmission here inside the loop. */

               if (slaves) flushSlavesOutputBuffers();

           }

       }

       if (!keys_freed) return REDIS_ERR; /* nothing to free... */

   }

   return REDIS_OK;

}


网络异常,图片无法展示
|

上图是Redis内存回收源码执行路径,具体步骤为:


  1. 内存空间定时任务轮询
  2. 检查used_memory > max_memory,满足则继续
  3. 判断maxmemory-policy内存回收策略
  4. 循环遍历数据库根据策略进行内存回收操作
  5. 如果存在slave从节点,则进行同步


综上,通过内存回收执行进行解读可以看到,内存回收过程需要循环遍历所有Redis数据库空间,且当存在SLAVE从节点时会有写放大问题,因此一般要尽量避免内存回收情况的出现。


内存优化


Redis是一个基于内存的Key-Value型数据库,因此内存是至关重要的,Redis在内存的使用上也做了很多优化。


redisObject对象


网络异常,图片无法展示
|

Redis在对象结构体设计上,通过 type 来指定对象数据类型( string 、list、hash、set、zset ), encoding 来指定对象编码实现,也就是底层数据结构实现( int、intset、ziplist、skiplist、linkedlist、hashtable )。
不同的底层数据结构实现也继承了这种灵活适配的设计思想,根据不同数据对象类型在不同存储空间要求的情况下进行底层数据结构的替换和适配,目的就是根据实际需要灵活分配内存空间,尽可能减少内存使用。


键值空间压缩


  • 对于键对象(Key)来说只能存储字符串对象,因此进来使用较短长度的字符串可以减少内存空间占用
  • 对于键对象(Value)来说可以存储各种复杂数据类型对象,但是最终存储值形式还是字符串数值,因此场景形式是对业务数据进行序列化存储以达到数据压缩存储的目的
    网络异常,图片无法展示
    |

    上图是一个Java中一些序列化工具的存储字节的对比,可以参考https://github.com/eishay/jvm-serializers/wiki


共享对象池


网络异常,图片无法展示
|

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


共享对象池失效的场景:


  • 当设置maxmemory并启用 LRU相关淘汰策略如:volatile-lruallkeys-lru时,Redis禁止使用共享对象池
  • 对于ziplist编码的值对象,即使内部数据为整数也无法使用共享对象池,因为ziplist使用压缩且内存连续的结构,对象共享判断成本过高


为什么只有整数对象池



综上可知,除了整数外,其他数据类型判断对象相等的时间复杂度非常高是非常耗费性能的操作。

数据类型

判断相等时间复杂度

整数

O(1)

字符串

O(n)

复杂数据结构对象

O(n2) 甚至更多


字符串优化


关于字符串的优化有两个地方要说明,一是Redis内部实现的SDS,可以参考 [Redis]数据结构与对象

数据类型

存储形式

空间占用

字符串

{"name":"zhangsan","age":30}

占用小

哈希

"key" : "name" "value" : "zhangsan"
"key" : "age" "value" : "30"

占用大


另外就是可以变化字符串的存储,Redis提供了固定的数据结构和实现,但是同样的数据在一般情况下可以进行数据结构变化,比如JSON串既可以存hash,也可以使用string,在数据结构都支持的情况下,可以先转换业务数据成最节省内存占用的数据结构方式来进行存储,个人习惯称之为降维打击


编码优化


不同类型对象具体底层实现的数据结构也不同, Redis 可以根据不同的使用场景来为一个对象设置不同的编码, 从而优化对象在某一场景下的效率,如下

网络异常,图片无法展示
|

类型

编码

对象

OBJECT ENCODING命令输出

REDIS_STRING

REDIS_ENCODING_INT

使用整数值实现的字符串对象

"int"

REDIS_STRING

REDIS_ENCODING_EMBSTR

使用 embstr 编码的简单动态字符串实现的字符串对象

"embstr"

REDIS_STRING

REDIS_ENCODING_RAW

使用简单动态字符串实现的字符串对象

"raw"

REDIS_LIST

REDIS_ENCODING_ZIPLIST

使用压缩列表实现的列表对象

"ziplist"

REDIS_LIST

REDIS_ENCODING_LINKEDLIST

使用双端链表实现的列表对象

"linkedlist"

REDIS_HASH

REDIS_ENCODING_ZIPLIST

使用压缩列表实现的哈希对象

"ziplist"

REDIS_HASH

REDIS_ENCODING_HT

使用字典实现的哈希对象

"hashtable"

REDIS_SET

REDIS_ENCODING_INTSET

使用整数集合实现的集合对象

"intset"

REDIS_SET

REDIS_ENCODING_HT

使用字典实现的集合对象

"hashtable"

REDIS_ZSET

REDIS_ENCODING_ZIPLIST

使用压缩列表实现的有序集合对象

"ziplist"

REDIS_ZSET

REDIS_ENCODING_SKIPLIST

使用跳跃表和字典实现的有序集合对象

"skiplist"


不支持编码回退
主要是数据增删频繁时,数据向压缩编码转换非常消耗CPU,得不偿失


参考


《Redis开发与运维》https://blog.csdn.net/jiangxiulilinux/article/details/104552852 Redis内存碎片率

https://www.cnblogs.com/remcarpediem/p/11755860.html

https://github.com/huangz1990/redis-3.0-annotated

https://github.com/eishay/jvm-serializers/wiki

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
1月前
|
存储 监控 算法
Java中的内存管理:理解Garbage Collection机制
本文将深入探讨Java编程语言中的内存管理,着重介绍垃圾回收(Garbage Collection, GC)机制。通过阐述GC的工作原理、常见算法及其在Java中的应用,帮助读者提高程序的性能和稳定性。我们将从基本原理出发,逐步深入到调优实践,为开发者提供一套系统的理解和优化Java应用中内存管理的方法。
|
21天前
|
存储 算法 Java
Go语言的内存管理机制
【10月更文挑战第25天】Go语言的内存管理机制
22 2
|
1月前
|
存储 缓存 NoSQL
大数据-45 Redis 持久化概念 RDB AOF机制 持久化原因和对比
大数据-45 Redis 持久化概念 RDB AOF机制 持久化原因和对比
39 2
大数据-45 Redis 持久化概念 RDB AOF机制 持久化原因和对比
|
23天前
|
存储 运维 Java
💻Java零基础:深入了解Java内存机制
【10月更文挑战第18天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
28 1
|
1月前
|
存储 缓存 NoSQL
Redis Quicklist 竟让内存占用狂降50%?
【10月更文挑战第11天】
42 2
|
1月前
|
设计模式 NoSQL 网络协议
大数据-48 Redis 通信协议原理RESP 事件处理机制原理 文件事件 时间事件 Reactor多路复用
大数据-48 Redis 通信协议原理RESP 事件处理机制原理 文件事件 时间事件 Reactor多路复用
37 2
|
1月前
|
存储 安全 NoSQL
driftingblues9 - 溢出ASLR(内存地址随机化机制)
driftingblues9 - 溢出ASLR(内存地址随机化机制)
36 1
|
1月前
|
程序员 编译器 数据处理
【C语言】深度解析:动态内存管理的机制与实践
【C语言】深度解析:动态内存管理的机制与实践
|
2月前
|
缓存 NoSQL 算法
14)Redis 在内存用完时会怎么办?如何处理已过期的数据?
14)Redis 在内存用完时会怎么办?如何处理已过期的数据?
56 0
|
2月前
|
存储 缓存 NoSQL
Redis 过期删除策略与内存淘汰策略的区别及常用命令解析
Redis 过期删除策略与内存淘汰策略的区别及常用命令解析
71 0