在Redis中的LRU算法文中说到,LRU有一个缺陷,在如下情况下:

~~~~~A~~~~~A~~~~~A~~~~A~~~~~A~~~~~A~~|

~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~|

~~~~~~~~~~C~~~~~~~~~C~~~~~~~~~C~~~~~~|

~~~~~D~~~~~~~~~~D~~~~~~~~~D~~~~~~~~~D|

会将数据D误认为将来最有可能被访问到的数据。

Redis作者曾想改进LRU算法,但发现Redis的LRU算法受制于随机采样数maxmemory_samples,在maxmemory_samples等于10的情况下已经很接近于理想的LRU算法性能,也就是说,LRU算法本身已经很难再进一步了。

于是,将思路回到原点,淘汰算法的本意是保留那些将来最有可能被再次访问的数据,而LRU算法只是预测最近被访问的数据将来最有可能被访问到。我们可以转变思路,采用一种LFU(Least Frequently Used)算法,也就是最频繁被访问的数据将来最有可能被访问到。在上面的情况中,根据访问频繁情况,可以确定保留优先级:B>A>C=D。

Redis中的LFU思路

在LFU算法中,可以为每个key维护一个计数器。每次key被访问的时候,计数器增大。计数器越大,可以约等于访问越频繁。

上述简单算法存在两个问题:

在LRU算法中可以维护一个双向链表,然后简单的把被访问的节点移至链表开头,但在LFU中是不可行的,节点要严格按照计数器进行排序,新增节点或者更新节点位置时,时间复杂度可能达到O(N)。

只是简单的增加计数器的方法并不完美。访问模式是会频繁变化的,一段时间内频繁访问的key一段时间之后可能会很少被访问到,只增加计数器并不能体现这种趋势。

第一个问题很好解决,可以借鉴LRU实现的经验,维护一个待淘汰key的pool。第二个问题的解决办法是,记录key最后一个被访问的时间,然后随着时间推移,降低计数器。

Redis对象的结构如下:

typedef struct redisObject {

unsigned type:4;

unsigned encoding:4;

unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or

* LFU data (least significant 8 bits frequency

* and most significant 16 bits access time). */

int refcount;

void *ptr;

} robj;

在LRU算法中,24 bits的lru是用来记录LRU time的,在LFU中也可以使用这个字段,不过是分成16 bits与8 bits使用:

16 bits 8 bits

+----------------+--------+

+ Last decr time | LOG_C |

+----------------+--------+

高16 bits用来记录最近一次计数器降低的时间ldt,单位是分钟,低8 bits记录计数器数值counter。

LFU配置

Redis4.0之后为maxmemory_policy淘汰策略添加了两个LFU模式:

volatile-lfu:对有过期时间的key采用LFU淘汰算法

allkeys-lfu:对全部key采用LFU淘汰算法

还有2个配置可以调整LFU算法:

lfu-log-factor 10

lfu-decay-time 1

lfu-log-factor可以调整计数器counter的增长速度,lfu-log-factor越大,counter增长的越慢。

lfu-decay-time是一个以分钟为单位的数值,可以调整counter的减少速度

源码实现

robj *lookupKey(redisDb *db, robj *key, int flags) {

dictEntry *de = dictFind(db->dict,key->ptr);

if (de) {

robj *val = dictGetVal(de);

/* Update the access time for the ageing algorithm.

* Don't do it if we have a saving child, as this will trigger

* a copy on write madness. */

if (server.rdb_child_pid == -1 &&

server.aof_child_pid == -1 &&

!(flags & LOOKUP_NOTOUCH))

{

if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {

updateLFU(val);

} else {

val->lru = LRU_CLOCK();

}

}

return val;

} else {

return NULL;

}

}

当采用LFU策略时,updateLFU更新lru:

/* Update LFU when an object is accessed.

* Firstly, decrement the counter if the decrement time is reached.

* Then logarithmically increment the counter, and update the access time. */

void updateLFU(robj *val) {

unsigned long counter = LFUDecrAndReturn(val);

counter = LFULogIncr(counter);

val->lru = (LFUGetTimeInMinutes()<<8) | counter;

}

降低LFUDecrAndReturn

首先,LFUDecrAndReturn对counter进行减少操作:

/* If the object decrement time is reached decrement the LFU counter but

* do not update LFU fields of the object, we update the access time

* and counter in an explicit way when the object is really accessed.

* And we will times halve the counter according to the times of

* elapsed time than server.lfu_decay_time.

* Return the object frequency counter.

*

* This function is used in order to scan the dataset for the best object

* to fit: as we check for the candidate, we incrementally decrement the

* counter of the scanned objects if needed. */

unsigned long LFUDecrAndReturn(robj *o) {

unsigned long ldt = o->lru >> 8;

unsigned long counter = o->lru & 255;

unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;

if (num_periods)

counter = (num_periods > counter) ? 0 : counter - num_periods;

return counter;

}

函数首先取得高16 bits的最近降低时间ldt与低8 bits的计数器counter,然后根据配置的lfu_decay_time计算应该降低多少。

LFUTimeElapsed用来计算当前时间与ldt的差值:

/* Return the current time in minutes, just taking the least significant

* 16 bits. The returned time is suitable to be stored as LDT (last decrement

* time) for the LFU implementation. */

unsigned long LFUGetTimeInMinutes(void) {

return (server.unixtime/60) & 65535;

}

/* Given an object last access time, compute the minimum number of minutes

* that elapsed since the last access. Handle overflow (ldt greater than

* the current 16 bits minutes time) considering the time as wrapping

* exactly once. */

unsigned long LFUTimeElapsed(unsigned long ldt) {

unsigned long now = LFUGetTimeInMinutes();

if (now >= ldt) return now-ldt;

return 65535-ldt+now;

}

具体是当前时间转化成分钟数后取低16 bits,然后计算与ldt的差值now-ldt。当ldt > now时,默认为过了一个周期(16 bits,最大65535),取值65535-ldt+now。

然后用差值与配置lfu_decay_time相除,LFUTimeElapsed(ldt) / server.lfu_decay_time,已过去n个lfu_decay_time,则将counter减少n,counter - num_periods。

增长LFULogIncr

增长函数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;

double p = 1.0/(baseval*server.lfu_log_factor+1);

if (r < p) counter++;

return counter;

}

counter并不是简单的访问一次就+1,而是采用了一个0-1之间的p因子控制增长。counter最大值为255。取一个0-1之间的随机数r与p比较,当r

+--------+------------+------------+------------+------------+------------+

| 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 |

+--------+------------+------------+------------+------------+------------+

可见counter增长与访问次数呈现对数增长的趋势,随着访问次数越来越大,counter增长的越来越慢。

新生key策略

另外一个问题是,当创建新对象的时候,对象的counter如果为0,很容易就会被淘汰掉,还需要为新生key设置一个初始counter,createObject:

robj *createObject(int type, void *ptr){

robj *o = zmalloc(sizeof(*o));

o->type= type;

o->encoding = OBJ_ENCODING_RAW;

o->ptr = ptr;

o->refcount = 1;

/* Set the LRU to the current lruclock (minutes resolution), or

* alternatively the LFU counter. */

if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {

o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;

} else {

o->lru = LRU_CLOCK();

}

return o;

}

counter会被初始化为LFU_INIT_VAL,默认5。

pool

pool算法就与LRU算法一致了:

if (server.maxmemory_policy & (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU) ||

server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL)

计算idle时有所不同:

} else if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {

/* When we use an LRU policy, we sort the keys by idle time

* so that we expire keys starting from greater idle time.

* However when the policy is an LFU one, we have a frequency

* estimation, and we want to evict keys with lower frequency

* first. So inside the pool we put objects using the inverted

* frequency subtracting the actual frequency to the maximum

* frequency of 255. */

idle = 255-LFUDecrAndReturn(o);

使用了255-LFUDecrAndReturn(o)当做排序的依据。

参考链接

lfu算法实现c语言_Redis中的LFU算法相关推荐

  1. 【每日算法】C语言8大经典排序算法(2)

    接上文--->[每日算法]C语言8大经典排序算法(1) 二.插入类排序 插入排序(Insertion Sort)的基本思想是:每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子文件中 ...

  2. 回溯算法:从电影蝴蝶效应中学习回溯算法的核心思想

    回溯算法:从电影<蝴蝶效应>中学习回溯算法的核心思想 数独.八皇后.0-1背包.图的着色.旅行商问题.全排列问题都能用到 理解"回溯算法" 回溯的思想,类似枚举搜索,枚 ...

  3. 滑动窗口滤波 c语言,关于中值滤波算法 以及C语言实现

    1.什么是中值滤波? 中值滤波是对一个滑动窗口内的诸像素灰度值排序,用其中值代替窗口中心象素的原来灰度值,它是一种非线性的图像平滑法,它对脉冲干扰级椒盐噪声的抑制效果好,在抑制随机噪声的同时能有效保护 ...

  4. 滑动窗口滤波 c语言,关于中值滤波算法,以及C语言实现

    1.什么是中值滤波? 中值滤波是对一个滑动窗口内的诸像素灰度值排序,用其中值代替窗口中心象素的原来灰度值,它是一种非线性的图像平滑法,它对脉冲干扰级椒盐噪声的抑制效果好,在抑制随机噪声的同时能有效保护 ...

  5. 滑动窗口滤波 c语言,关于中值滤波算法,以及C语言实现(转)

    1.什么是中值滤波? 中值滤波是对一个滑动窗口内的诸像素灰度值排序,用其中值代替窗口中心象素的原来灰度值,它是一种非线性的图像平滑法,它对脉冲干扰级椒盐噪声的抑制效果好,在抑制随机噪声的同时能有效保护 ...

  6. louvian算法 缺点 优化_机器学习中的优化算法(1)-优化算法重要性,SGD,Momentum(附Python示例)...

    本系列文章已转至 机器学习的优化器​zhuanlan.zhihu.com 优化算法在机器学习中扮演着至关重要的角色,了解常用的优化算法对于机器学习爱好者和从业者有着重要的意义. 这系列文章先讲述优化算 ...

  7. luhn算法java_Java信用卡验证– Java中的Luhn算法

    luhn算法java Recently I came to know that Credit Card numbers are not random and passes Luhn Algorithm ...

  8. zuc算法c需语言,3GPP机密性和完整性算法规范128-EEA3和128-EIA3(二)-祖冲之算法的C语言实现 - 小黑电脑...

    3GPP机密性和完整性算法规范128-EEA3和128-EIA3(一)----密钥生成原理 3GPP机密性和完整性算法规范128-EEA3和128-EIA3(二)----祖冲之算法的C语言实现 3GP ...

  9. python中值滤波算法_Python扩展库scipy中值滤波算法的应用

    中值滤波是数字信号处理.数字图像处理中常用的预处理技术,特点是将信号中每个值都替换为其邻域内的中值,即邻域内所有值排序后中间位置上的值.下面的代码演示了scipy库中signal模块的中值滤波算法的用 ...

最新文章

  1. Windows下安装 openpyxl
  2. 最强轻量级目标检测yolo fastest
  3. 单核工作法9:消减待办任务
  4. Guava - 拯救垃圾代码,写出优雅高效,效率提升N倍
  5. C语言学习笔记---随机数rand()函数
  6. Redis问题:ERR max number of clients reached
  7. linux下qemu共享文件夹,QEMU Windows来宾和Linux主机之间的共享文件夹
  8. 在线词云工具Tagxedo的使用
  9. Linux内核有加网速功能吗,Linux下使用有线网络和WiFi能不能叠加网速?网友评论不一...
  10. python打印皮卡丘步骤_编程作战丨如何利用python绘制可爱皮卡丘?
  11. JSP常用标记——(web基础学习笔记)
  12. 成人职业教育:知乎、B站、网易“短兵相接”
  13. 51单片机智能远程遥控温控PWM电风扇系统红外遥控温度速度定时关机
  14. matlab做的脑网络分析,eeg脑网络分析
  15. (转)ANDROID强制锁定竖屏_APP固定设置竖屏或横屏
  16. 2022浙江省计算机三级网络及安全技术考试自学资料(3)(更新于3.9)
  17. linux 查看主板sn_Linux系统查看硬件信息
  18. 【Flutter 问题系列第 22 篇】在 Flutter 中如何截取屏幕并显示到页面中,以及如何将截图保存到相册
  19. spring boot 配置logback,logback不打印日志
  20. 小程序如何关联企业微信

热门文章

  1. Postman HTTP 400 Bad Request及模拟@RequestParam请求
  2. oracle 差分备份,Oracle数据库RMAN备份与恢复:备份与恢复(手机搬家)
  3. typora简历主题推荐
  4. 任务启动需要经历九九八十一难,没有大师兄带路,咱们依旧可以取得真经
  5. c语言字符-1代表什么,玩儿转C语言:符号和字符(1)
  6. 如何在vscode 中打开新文件夹不覆盖上一个窗口标签
  7. 【SOLIDWORKS学习笔记】装配体基础操作
  8. Qt设置边框阴影效果
  9. Echarts柱状图3d立体效果
  10. Python——遍历整个列表