Redis LFU 实现 -- 掷硬币的艺术

本文深入探讨Redis中的LFU(Least Frequently Used)算法实现,包括其原理、近似计数算法Morris算法以及Redis的具体实现细节,如访问计数和计数衰减。通过对LFU算法的理解,揭示Redis如何优雅地处理缓存淘汰问题。
摘要由CSDN通过智能技术生成

最近看了Redis的LFU缓存淘汰的实现,被惊艳到了。同样是敲代码,有人把它上升成了艺术。

Redis在4.0之后支持配置缓存淘汰使用LFU(Least Frequently Used, 最少使用算法)。先简单回顾下LFU算法:

LFU算法

LFU是缓存淘汰算法。一般我们可用于缓存的空间有限,但是我们需要缓存的数据可能是无限,为了能够提升缓存的命中率,我们希望留在缓存中的会是我们之后较短时间内最可能有到的那部分,这时候缓存淘汰就有用武之地了。

LFU算法的思路是: 我们记录下来每个(被缓存的/出现过的)数据的请求频次信息,如果一个请求的请求次数多,那么它就很可能接着被请求。

在数据请求模式比较稳定(没有对于某个数据突发的高频访问这样的不稳定模式)的情况下,LFU的表现还是很不错的。在数据的请求模式大多不稳定的情况下,LFU一般会有这样一些问题:

1)微博热点数据一般只是几天内有较高的访问频次,过了这段时间就没那么大意义去缓存了。但是因为在热点期间他的频次被刷上去了,之后很长一段时间内很难被淘汰;
2)如果采用只记录缓存中的数据的访问信息,新加入的高频访问数据在刚加入的时候由于没有累积优势,很容易被淘汰掉;
3)如果记录全部出现过的数据的访问信息,会占用更多的内存空间。

对于上面这些问题,其实也都有一些对应的解决方式,相应的出现了很多LFU的变种。如:Window-LFU、LFU*、LFU-Aging。在Redis的LFU算法实现中,也有其解决方案。

近似计数算法 – Morris算法

Redis记录访问次数使用了一种近似计数算法——Morris算法。Morris算法利用随机算法来增加计数,在Morris算法中,计数不是真实的计数,它代表的是实际计数的量级。

算法的思想是这样的:算法在需要增加计数的时候通过随机数的方式计算一个值来判断是否要增加,算法控制 在计数越大的时候,得到结果“是”的几率越小

escape

我们来玩个逃脱游戏,游戏规则是这样的:
1) 你被困在一个倒金字塔结构的地下7层建筑里头,金字塔从底向上我们分别标号0、1、2、3、4、5、6;
2) 每层的天花板被划分成 1 0 n 10^n 10n个大小相等的区块, n n n为当层的标号(0层1块,1层10块,2层100块 …),这些区块的从1到 1 0 n 10^n 10n进行编号(随机,9号编号盘的可能是232,且从下往上看天花板看不到编号);
3) 每层天花板上的 1 0 n 10^n 10n个区块中有 1 0 n − 1 10^n - 1 10n1块被铺上了坚硬的地板砖,仅剩下的一个区块(被随机选定的)是一个升降机,没错,它就是带你逃离这层的,但是它看着和其他地板砖没任何差别;
5) 每层有一个摇号机,通过这个摇号机能够得到一个1到 1 0 n 10^n 10n间的号,当摇到的号是升降机所在的区块,升降机将会降下来,带你离开这层;
4) 你被困在最底层 – 0层,站在摇号机边,开始你的游戏。
从上面的游戏看,最顺利的情况下,经过7次摇号,你就能逃离了。但是实际呢?概率是个很神奇的东西。

在这里插入图片描述 在这里插入图片描述
在这里插入图片描述 在这里插入图片描述
在这里插入图片描述

上面按游戏规则试了10次的结果的图(都是一张图,后面几张是为了能看清楚,对某个范围进行了放大的结果)。横轴为楼层数,纵轴为达到对应楼层尝试的次数。

从上面的图能看到, 曲线整体都是越来约陡的,也就是想要得到“是”越来越难。虽然对于同一层,几次游戏可能使用的次数会不同,但是基本可以认为是在一个大致范围内的。按着上面的图,我们可以说:如果你到了第三层,你应该尝试了上千次。这个时候,3这个数字已经不仅仅代表3了,它代表的是一个量级,代表的一个近似计数。

近似计数算法一个很重要的参数就是概率的变化,上面的游戏里,概率一直以上层的0.1降低,如果这个降低的速率更快,比如是上层的概率的0.05,那么可以想象,想要上一层变得更难了,上一层楼我要试的次数会更多,那么曲线会变得更陡,能标识得近似数范围也会变得更大。

Redis中的LFU实现

Redis中关于缓存淘汰的核心源码都在evict.c文件中,其中实现LFU的主要方法是:LFUGetTimeInMinutesLFUTimeElapsedLFULogIncrLFUDecrAndReturn。下面具体来说:

Redis中实现LFU算法的时候,有这个两个重要的可配置参数:

  • server.lfu_log_factor : 能够影响计数的量级范围,即上表中的factor参数;
  • server.lfu_decay_time: 控制LFU计数衰减的参数。

关于这两个参数的作用接着往下看。

访问计数

前面说了Redis的LFU实现使用了近似计数算法,这个算法的特点是能够用一个较小的数表示一个很大的量级,所以对于Redis来说统计频次不需要太多空间和内容,只需要一个不那么大的数就行(这个特性解决了前面说的LFU的常见问题3)。正好,Redis的LRU算法实现里,用了一个24位的redisObject->lru字段,拿到LFU中正好合用。Redis没有全部用掉这24位,只拿了其中8位用来做计数,剩下的16位另作别用。

 *          16 bits      8 bits
 *     +----------------+--------+
 *     + Last decr time | LOG_C  |
 *     +----------------+--------+

8个bit位最大为255,从Redis文档中贴出来的数据(如下表)可以看到,不同的factor的值能够控制计数代表的量级的范围,当factor为100时,能够最大代表10M,也就是千万级别的命中数。

factor 100 hits 1000 hits 100K hits 1M hits 10M hits
0 104 255 255 255 255
1 18 49 255 255 255
10 10 18 142 255 255
100 8 11 49 143 255

第一列的factor,就是前头说的配置项__server.lfu_log_factor__,那么这个配置的作用就比较明显了,就是用来控制概率衰减的速率。具体怎么控制的,我们来看下LFULogIncr方法的实现:

/* Logarithmically increment a counter. The greater is the current counter value
 * the less likely is that it gets really implemented. Saturate it at 255. */
uint8_t LFULogIncr(uint8_t counter) {
   
    if (counter == 255) return 255;
    double r = (double)rand()/RAND_MAX;
    double baseval = counter - LFU_INIT_VAL;
    if (baseval < 0) baseval = 0
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值