深入解析 Redis 的 LRU 与 LFU 算法实现
作者:vivo 互联网服务器团队 - Luo Jianxin
重点介绍了 Redis 的 LRU 与 LFU 算法实现,并分析总结了两种算法的实现效果以及存在的问题。
一、前言
Redis 是一款基于内存的高性能 NoSQL 数据库,数据都缓存在内存里, 这使得 Redis 可以每秒轻松地处理数万的读写请求。
相对于磁盘的容量,内存的空间一般都是有限的,为了避免 Redis 耗尽宿主机的内存空间,Redis 内部实现了一套复杂的缓存淘汰策略来管控内存使用量。
Redis 4.0 版本开始就提供了 8 种内存淘汰策略,其中 4 种都是基于 LRU 或 LFU 算法实现的,本文就这两种算法的 Redis 实现进行了详细的介绍,并阐述其优劣特性。
二、Redis 的 LRU 实现
在介绍 Redis LRU 算法实现之前,我们先简单介绍一下原生的 LRU 算法。
2.1 LRU 算法原理
LRU(The Least Recently Used)是最经典的一款缓存淘汰算法,其原理是 :如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很低,当数据所占据的空间达到一定阈值时,这个最少被访问的数据将被淘汰掉。
如今,LRU 算法广泛应用在诸多系统内,例如 Linux 内核页表交换,MySQL Buffer Pool 缓存页替换,以及 Redis 数据淘汰策略。
以下是一个 LRU 算法示意图:
向一个缓存空间依次插入三个数据 A/B/C,填满了缓存空间;
读取数据 A 一次,按照访问时间排序,数据 A 被移动到缓存头部;
插入数据 D 的时候,由于缓存空间已满,触发了 LRU 的淘汰策略,数据 B 被移出,缓存空间只保留了 D/A/C。
一般而言,LRU 算法的数据结构不会如示意图那样,仅使用简单的队列或链表去缓存数据,而是会采用 Hash 表 + 双向链表的结构,利用 Hash 表确保数据查找的时间复杂度是 O(1),双向链表又可以使数据插入/删除等操作也是 O(1)。
如果你很熟悉 Redis 的数据类型,你会发现这个 LRU 的数据结构与 ZSET 类型 OBJ_ENCODING_SKIPLIST 编码结构相似,只是 LRU 数据排序方式更简单一些。
2.2 Redis LRU 算法实现
按照官方文档的介绍,Redis 所实现的是一种近似的 LRU 算法,每次随机选取一批数据进行 LRU 淘汰,而不是针对所有的数据,通过牺牲部分准确率来提高 LRU 算法的执行效率。
Redis 内部只使用 Hash 表缓存了数据,并没有创建一个专门针对 LRU 算法的双向链表,之所以这样处理也是因为以下几个原因:
筛选规则,Redis 是随机抽取一批数据去按照淘汰策略排序,不再需要对所有数据排序;
性能问题,每次数据访问都可能涉及数据移位,性能会有少许损失;
内存问题,Redis 对内存的使用一向很“抠门”,数据结构都很精简,尽量不使用复杂的数据结构管理数据;
策略配置,如果线上 Redis 实例动态修改淘汰策略会触发全部数据的结构性改变,这个 Redis 系统无法承受的。
redisObject 是 Redis 核心的底层数据结构,成员变量 lru 字段用于记录了此 key 最近一次被访问的 LRU 时钟(server.lruclock),每次 Key 被访问或修改都会引起 lru 字段的更新。
默认的 LRU 时钟单位是秒,可以修改 LRU_CLOCK_RESOLUTION 宏来改变单位,LRU 时钟更新的频率也和 server.hz 参数有关。
由于 lru 字段仅占用了 24bit 的空间,按秒为单位也只能存储 194 天,所以可能会出现一个意想不到的结果,即间隔 194 天访问 Key 后标记的时间戳一样,Redis LRU 淘汰策略局部失效。
2.3 LRU 算法缺陷
LRU 算法仅关注数据的访问时间或访问顺序,忽略了访问次数的价值,在淘汰数据过程中可能会淘汰掉热点数据。
如上图所示,时间轴自左向右,数据 A/B/C 在同一段时间内被分别访问的数次。数据 C 是最近一次访问的数据,按照 LRU 算法排列数据的热度是 C>B>A,而数据的真实热度是 B>A>C。
这个是 LRU 算法的原理性问题,自然也会在 Redis 近似 LRU 算法中呈现,为了解决这个问题衍生出来 LFU 算法。
三、Redis 的 LFU 实现
3.1 LFU 算法原理
LFU(Least frequently used)即最不频繁访问,其原理是:如果一个数据在近期被高频率地访问,那么在将来它被再访问的概率也会很高,而访问频率较低的数据将来很大概率不会再使用。
很多人看到上面的描述,会认为 LFU 算法主要是比较数据的访问次数,毕竟访问次数多了自然访问频率就高啊。实际上,访问频率不能等同于访问次数,抛开访问时间谈访问次数就是在“耍流氓”。
在这段时间片内数据 A 被访问了 5 次,数据 B 与 C 各被访问了 4 次,如果按照访问次数判断数据热度值,必然是 A>B=C;如果考虑到时效性,距离当前时间越近的访问越有价值,那么数据热度值就应该是 C>B>A。因此,LFU 算法一般都会有一个时间衰减函数参与热度值的计算,兼顾了访问时间的影响。
LFU 算法实现的数据结构与 LRU 一样,也采用 Hash 表 + 双向链表的结构,数据在双向链表内按照热度值排序。如果某个数据被访问,更新热度值之重新插入到链表合适的位置,这个比 LRU 算法处理的流程复杂一些。
3.2 Redis LFU 算法实现
Redis 4.0 版本开始增加了 LFU 缓存淘汰策略,也采用数据随机筛选规则,然后依据数据的热度值排序,淘汰掉热度值较低的数据。
3.2.1 LFU 算法代码实现
LFU 算法的实现没有使用额外的数据结构,复用了 redisObject 数据结构的 lru 字段,把这 24bit 空间拆分成两部分去使用。
由于记录时间戳在空间被压缩到 16bit,所以 LFU 改成以分钟为单位,大概 45.5 天会出现数值折返,比 LRU 时钟周期还短。
低位的 8bit 用来记录热度值(counter),8bit 空间最大值为 255,无法记录数据在访问总次数。
LFU 热度值(counter)的算法实现:
counter 小于或等于 LFU_INIT_VAL 时候,数据一旦被访问命中, counter 接近 100%概率递增 1;
counter 大于 LFU_INIT_VAL 时候,需要先计算两者差值,然后作为分母的一部分参与递增概率的计算;
随着 counter 数值的增大,递增的概率逐步衰减,可能数次的访问都不能使其数值加 1;
当 counter 数值达到 255,就不再进行数值递增的计算过程。
LFU counter 的计算也并非“一尘不变”,为了适配各种业务数据的特性,Redis 在 LFU 算法实现过程中引入了两个可调参数:
阅读完以上的内容,是否感觉似曾相似?实际上 LFU counter 计算过程就是对访问次数进行了数值归一化,将数据访问次数映射成热度值(counter),数值的范围也从[0,+∞)映射到另一个维度的[0,255]。
3.3.2 LFU Counter 分析
仅从代码层面分析研究 Redis LFU 算法实现会比较抽象且枯燥,无法直观的呈现 counter 递增概率的算法效果,以及 counter 数值与访问次数的关系。
在 lfu_log_factor 为默认值 10 的场景下,利用 Python 实现 Redis LFU 算法流程,绘制出 LFU counter 递增概率曲线图:
可以清晰的观察到,当 LFU counter 数值超过 LFU_INIT_VAL 之后,曲线出现了垂直下降,递增概率陡降到 0.2%左右,随后在底部形成一个较为缓慢的衰减曲线,直至 counter 数值达到 255 则递增概率归于 0,贴合 3.3.1 章节分析的理论。
保持 Redis 系统配置默认值的情况下,对同一个数据持续的访问,并采集此数据的 LFU counter 数值,绘制出 LFU counter 数值曲线图:
随着访问次数的不断增加,LFU counter 数值曲线呈现出爬坡式的递增,形态趋近于根号曲线,由此推测出以下观点:
在访问次数相同的情况下,counter 数值不是固定的,大概率在一个范围内波动;
在同一个时间段内,数据之间访问次数相差上千次,才可以通过 counter 数值区分出哪些数据更热,而“温”数据之间可能很难区分热度。
四、总结
通过对 Redis LRU 与 LFU 算法实现的介绍,我们可以大体了解两种算法策略的优缺点,在 Redis 运维过程中,可以依据业务数据的特性去选择相应的算法。
如果业务数据的访问较为均匀,OPS 或 CPU 利用率一般不会出现周期性的陡升或陡降,数据没有体现出相对的“冷热”特性,即建议采用 LRU 算法,可以满足一般的运维需求。
相反,业务具备很强时效性,在活动推广或大促期间,业务某些数据会突然成为热点数据,监控上呈现出 OPS 或 CPU 利用率的大幅波动,为了能抓取热点数据便于后期的分析或优化,建议一定要配置成 LFU 算法。
在 Used_memory 接近 Maxmemory 的情况下,Redis 一直都采用随机的方式筛选数据,且筛选的个数极其有限,所以,LFU 算法无法展现出较大的优势,也可能会淘汰掉比较热的数据。
参考文献:
版权声明: 本文为 InfoQ 作者【vivo互联网技术】的原创文章。
原文链接:【http://xie.infoq.cn/article/6fa3a5c2cc41a2bca6398d826】。文章转载请联系作者。
评论