Redis 内存管理赏析

  • 时间: 2018-09-23 06:22:34

声明:本文是对 4.0.6版本 Redis的 内存管理部分的(xuexi)总结,有些YY的成分,作者本意不想误导, 如有错误, 敬请谅解。

一、凡事先问个为什么

Redis是互联网公司主流的分布式缓存解决方案,缓存的本质是“ 就近暂存 ”,解决的是“ 减少去较远的地方拿我想要的东西要耗费较大成本 ”的问题。本小节我们要探究的不是为什么要用缓存,而是Redis为什么要做内存管理这件事情。决定一件事情做不做的理由 绝对不是 “这件事情可以做”,我认为Redis做内存管理的主要原因是: 内存(RAM)资源宝贵,内存管理让内存的使用情况可控

二、遵循底线、条条大路通罗马

那么怎么做内存管理呢?管理包括对个体的管理和对总体的管理,个体上比如对Redis中存储的每个对象的大小,质量等进行管理;Redis的做法遵循 简单原则 ,在内存使用的总量上划一道“ 允许最大使用内存量 ”这条底线。为了遵循这条底线,Redis在实现上是这么做的:当使用内存量高于这条底线的时候,所有的写操作将会直接失败,绝不手软,但是这个时候仍然理智地支持读操作。在底线的基础之上Redis提供的管理手段是“淘汰机制”,注意 淘汰机制是管理的一种手段 。那么怎么淘汰呢?按大小?按年龄?Redis提供以下几种选择淘汰KEY的策略:

LRU: 看看哪个KEY最长时间没有被使用啦,就选你吧,你看你这么长时间都没被使用了,就别站着地方了。

LFU: 如果说上面的方法较为片面,那这里就加上一个期限,看看哪个KEY在一段时间内访问的频率最低好吧,谁最低,淘汰谁。

RANDOM: 不想那么多了,整个世界都是随机的,随机的世界就随机选择一个KEY吧,爱咋咋地。

TTL: 选择最近即将过期的KEY进行淘汰。

最后一种比较有意思,那就是—— 不淘汰 ,哈哈, 对于一个问题不去解决往往也是一种解决方案

三、难易相成、长短相形

在主从模式且主库开启maxmemory策略的情况下,对于与主库来说,如果用于从库同步的缓冲区也被作为内存使用量计算进来,在某些极端情况下,比如:主从之间网络出现异常,这个时候内存使用又达到阈值,显然Redis此时会进行eviction操作,eviction操作本身会产生大量的DELs操作日志用于同步给从库,这部分日志有会填充在主从同步缓存中,由于这部分内存会被计算进内存使用量,因此又会触发eviction操作,然后继续恶性循环……,直至最后内存被清空。 引入一种方案时长会伴随着引入新的问题和忧患,这需要我们提高警惕 ,那么怎么解决这个问题呢?Redis的做法很简单,就是用于从库同步的缓冲区内存大小不作为使用内存计算的,这样就不会存在上述问题了。在实际使用中,当以主从模式使用Redis且开启了maxmemory策略的时候,建议maxmemory这个值设置稍微小一些,预留一部分给主动同步缓冲区使用。

四、损有余、补不足

在实际实现LRU、LFU和最小TTL算法的时候面临一些问题,比如LRU算法要求淘汰最长时间没有使用的KEY,如果要精准满足“ 最长时间没有使用 ”这个条件,我们必须要遍历所有的KEY然后才能根据其上次使用的时间计算出哪个KEY是最长时间没有被访问的。问题来了,实际上Redis中可能存在成千上万个KEY,每次都要挨个遍历岂不是要疯了? 实际想一想精确真的是我们想要的吗? 实际Redis实现的LRU、LFU和最小TTL算法都是基于近似算法计算的,用 精确度换时间 (扩展一下hyperloglog是用准确性换空间),关键算法是:默认情况下Redis每次只随机选择5(可以通过maxmemory-samples参数设置,maxmemory-samples越大的时候越消耗内存,maxmemory-samples越小的时候越快,但是越不精确)个KEY,然后从这5个KEY中找到最符合条件的,这个思想其实也类似选择择局部最优,放弃全局最优的贪心算法。

五、精益求精、追求卓越

无论是LRU、LFU还是TTL都是基于系统时间的,在一般操作系统中,读取系统的当前时间需要进行system call,system call意味着操作系统需要陷入内核态,陷入内核态意味着用户态与内核态的上下文切换,上下文切换意味着成本……。为了实现内存淘汰,Redis需要为每个KEY都维护着一个这样的时间,而且在KEY被访问的时候需要读取操作系统当前时间将其更新,试想一下,在上W QPS的应用场景下,每次都要去system call在高性能的场景下是一件多么恐怖的事情。实际Redis是怎么处理的呢?这里有个背景,Redis所谓的单线程指的是Redis对于客户端读写的处理和内部的一些日常事务处理都是由一个eventloop线程来处理的,这里的日常事务包括一项:每次读取当前系统时间记录在一个变量里面。一般情况Redis所读取的时间都是从这里读取的,也就是由于eventloop线程提前计算好的,当然如果eventloop的loop频率比较低的话这个值将会具有一定延迟。