当前位置:首页 > 问答 > 正文

Redis内存分配那些事儿,聊聊它到底咋管理内存的

主要参考自Redis官方文档、博客以及《Redis设计与实现》一书)

说起Redis,大家都知道它快,像个闪电侠,但这份速度,很大程度上是用内存换来的,数据都放在内存里,不用慢吞吞地去硬盘里翻找,能不快吗?Redis怎么管好自己这一亩三分地(内存)就显得特别重要了,今天咱们就聊聊,Redis这家伙到底是怎么当这个“内存大管家”的。

第一件事:它用什么方式装下你的数据?

你往Redis里塞一个键值对,set name "张三",Redis可不是随便找块空地就给你扔那儿,它有自己的“收纳盒”,这个盒子的名字叫内存分配器,Redis早期版本直接用电脑操作系统自带的内存分配方法(比如malloc),但后来发现不行,操作系统那套太通用了,对于Redis这种频繁申请释放小块内存的场景,效率不高还容易产生内存碎片。

Redis的开发者自己搞了一个更适合自己的分配器,叫 jemalloc,或者也可以选择 tcmalloc,你可以把它们想象成专门为Redis定制的高级收纳师,它们是怎么工作的呢?简单说就是“批发转零售”,jemalloc会先向操作系统申请一大块连续的内存(比如4MB),这一大块叫做一个“内存页”,当Redis需要存储一个小的键值对时,jemalloc就从自己管理的这个“大仓库”里切一小块给你用,这样做的好处太多了:向操作系统申请内存的次数大大减少,因为是一次批发,多次零售;管理自己仓库里的内存碎片要比管理整个系统的内存碎片容易得多,这就好比你自己有个工具箱,东西都归置得井井有条,总比每次要用锤子都去整个杂货间里翻要快得多。(来源:Redis官方文档关于内存优化的章节)

第二件事:数据过期了怎么办?总不能一直占着地方吧?

你肯定用过Redis的过期时间功能,setex key 60 value,让数据一分钟后自动消失,这是个好功能,但背后有个问题:Redis怎么知道哪些数据过期了并及时清理掉,把内存腾出来呢?它可不是每分钟都把所有的数据检查一遍,那太慢了,如果数据量特别大,扫描一遍会卡死整个服务。

Redis用了两种策略结合的办法,像个精明的保洁阿姨:

Redis内存分配那些事儿,聊聊它到底咋管理内存的

  1. 惰性删除:这是主菜,当客户端试图访问一个键的时候,Redis才会顺便检查一下这个键是否过期了,如果过期,就立刻删除,然后告诉客户端“这个键不存在”,这招非常高效,因为只会在真正被用到的时候才做检查,但缺点也很明显:如果一个键永远不再被访问,即使它早过期了,也会一直赖在内存里,成了“僵尸数据”,白占地方。

  2. 定期删除:这是对惰性删除的补充,因为怕“僵尸数据”太多,Redis会每隔一段时间(默认是每秒10次,可配置)随机抽取一部分设置了过期时间的键进行检查,并删除其中过期的,注意,是“随机抽检”一部分,而不是全部,所以对CPU影响很小,通过多次的随机抽检,最终能比较及时地清理掉大部分过期数据。

这两种策略配合,就在性能和内存回收之间取得了一个很好的平衡。(来源:《Redis设计与实现》中关于键过期策略的详解)

第三件事:内存快满了,Redis的“断舍离”大招

Redis内存分配那些事儿,聊聊它到底咋管理内存的

当Redis占用的内存达到你设置的上限时(通过 maxmemory 参数配置),真正的考验就来了,新的数据要进来,但没地方放了,怎么办?这时候,Redis不会自作主张地崩溃,而是会启动它的内存淘汰策略,这个策略就像是在家里空间不够时,决定扔掉哪些东西来腾地方,你可以根据你的业务需求,选择不同的淘汰策略,常见的有以下几种:

  • noeviction:直接拒绝所有写入请求,只允许读操作,这是默认策略,最省心,但可能会让你的服务看起来像“挂了一样”。
  • allkeys-lru:这是最常用的策略之一,LRU是“最近最少使用”的意思,Redis会尝试淘汰掉所有键中,最长时间没有被访问过的那个键,这比较符合常识,不常用的东西就先扔了,Redis用的是一种近似的LRU算法,不是完全精确的,但用很少的内存开销就获得了非常接近的效果,性价比极高。
  • volatile-lru:只从那些设置了过期时间的键中,淘汰最近最少使用的。
  • allkeys-random:简单粗暴,随机淘汰一个键。
  • volatile-ttl:从设置了过期时间的键中,淘汰剩余寿命最短的那个。

选择哪种策略,取决于你的数据的重要性,如果你的数据都没设过期时间,那肯定不能用volatile开头的策略,如果你的数据访问有热点,allkeys-lru通常是个不错的选择。(来源:Redis官方文档对maxmemory-policy配置的说明)

第四件事:怎么让内存更紧凑?对付“空间浪费”

即使有了优秀的分配器和淘汰策略,还有一个隐形杀手——内存碎片,这就像你衣柜里,虽然衣服总量没变,但大衣和小袜子之间留下了很多小空隙,导致一件新大衣塞不进去了,内存碎片也是这样产生的:比如你频繁地修改数据,新写入的数据可能比旧数据大或者小,导致分配的内存块大小不匹配,从而产生很多无法被利用的小空隙。

Redis怎么解决这个问题呢?从4.0版本开始,它提供了一个“大扫除”功能:内存碎片整理,你可以通过配置开启它,Redis会在后台慢慢地、一点点地把数据拷贝到新的、更紧凑的内存位置,从而把碎片合并起来,这个过程是渐进式的,不会严重阻塞正常服务,但可能会稍微增加一点CPU的负担,这又是一个需要根据实际情况权衡的开关。(来源:Redis 4.0 release notes 以及相关博客对ACTIVE DEFRAGMENTATION的介绍)

Redis这个“内存大管家”当得是相当称职的,它通过定制化的内存分配器(jemalloc/tcmalloc) 来高效地分配内存;用惰性删除加定期删除的组合拳来清理过期数据;在内存不足时,通过可配置的淘汰策略来做出艰难的取舍;还提供了碎片整理工具来优化内存空间,理解了这几件事,你就能更好地驾驭Redis,让它既跑得快,又不会因为内存问题而“掉链子”。