Redis中的LFU算法
在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的减少速度
源码实现
在lookupKey中:
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<p时,才增加counter,这和比特币中控制产出的策略类似。p取决于当前counter值与lfu_log_factor因子,counter值与lfu_log_factor因子越大,p越小,r<p的概率也越小,counter增长的概率也就越小。增长情况如下:
+--------+------------+------------+------------+------------+------------+
| 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)当做排序的依据。
参考链接
Redis中的LFU算法的更多相关文章
- 动手实现 LRU 算法,以及 Caffeine 和 Redis 中的缓存淘汰策略
我是风筝,公众号「古时的风筝」. 文章会收录在 JavaNewBee 中,更有 Java 后端知识图谱,从小白到大牛要走的路都在里面. 那天我在 LeetCode 上刷到一道 LRU 缓存机制的问题, ...
- Redis 中的过期删除策略和内存淘汰机制
Redis 中 key 的过期删除策略 前言 Redis 中 key 的过期删除策略 1.定时删除 2.惰性删除 3.定期删除 Redis 中过期删除策略 从库是否会脏读主库创建的过期键 内存淘汰机制 ...
- Redis 中常见的集群部署方案
Redis 的高可用集群 前言 几种常用的集群方案 主从集群模式 全量同步 增量同步 哨兵机制 什么是哨兵机制 如何保证选主的准确性 如何选主 选举主节点的规则 哨兵进行主节点切换 切片集群 Redi ...
- Redis中的数据结构
1. 底层数据结构, 与Redis Value Type之间的关系 对于Redis的使用者来说, Redis作为Key-Value型的内存数据库, 其Value有多种类型. String Hash L ...
- Redis中的LRU淘汰策略分析
Redis作为缓存使用时,一些场景下要考虑内存的空间消耗问题.Redis会删除过期键以释放空间,过期键的删除策略有两种: 惰性删除:每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除 ...
- 关于Redis中的serverCron
1.serverCron简介 在 Redis 中, 常规操作由 redis.c/serverCron 实现, 它主要执行以下操作 /* This is our timer interrupt, cal ...
- 超大批量删除redis中无用key+配置
目前线上一个单实例redis中无用的key太多,决定删除一部分. 1.删除指定用户的key,使用redis的pipeline 根据一定条件把需要删除的用户统计出来,放到一个表里面,表为 del_use ...
- Redis中的数据对象
redis对象 redis中有五种常用对象 我们所说的对象的类型大多是值的类型,键的类型大多是字符串对象,值得类型大概有以下几种,但是无论哪种都是基于redisObject实现的 redisObjec ...
- redis中key的过期键删除策略
Redis过期键删除策略 Redis key过期的方式有三种: 被动删除:当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key 主动删除:由于惰性删除策略无法保证冷数据被及时删 ...
随机推荐
- CSS选择器分类整理
读完<CSS权威指南>,对选择器有如下整理: selector 本质上是执行元素样式匹配规则,我们可以利用它来限定某些元素 选择器类型: <!-- 实体,即HTML中存在 --> ...
- LEDE安装zerotier 无法启动解决方法
进入命令行模式,vi /etc/config/zerotier文件,把option enabled '0'的0改为1.
- 这样配置:让你的 IDEA 好用到飞起来
阅读本文大概需要 7 分钟. 来源:blog.csdn.net/fly910905/article/details/77868300 1.设置maven 1.在File->settings-&g ...
- 微服务异步架构---MQ之RocketMQ
“我们大家都知道把一个微服务架构变成一个异步架构只需要加一个MQ,现在市面上有很多MQ的开源框架.到底选择哪一个MQ的开源框架才合适呢?” 一.什么是MQ?MQ的原理是什么? MQ就是消息队列,是Me ...
- 2019-2020-1 20181218《Linux内核原理与分析》第一周作业
目录 问题和参考 有趣/有用的Linux命令 小作业和解答 一些图片 相关学习 本文是学习Linux基础入门(新版)后的总结. 问题和参考 虚拟机莫名崩溃,一天重启了几十次电脑,重装两次系统,心累.想 ...
- Kubeadm部署安装kubernetes1.12.1
1.环境准备(这里是master) CentOS 7.6 两台配置如下,自己更改主机名,加入hosts, master和node 名字不能一样 # hostname master # hostname ...
- VS2019调试 asp.net core 2.2 出现《ANCM In-Process Handler Load Failure 发布后启动错误处理》处理
从 google 出来的 github 上 AspNetCore issues 和 stackoverflow 搜到的,百度 博客园搜到的,CSDN 搜到的,统统设置了,不管用. 从这些问题 ...
- 【C/C++开发】c++ 工具库 (zz)
下面是收集的一些开发工具包,主要是C/C++方面的,涉及图形.图像.游戏.人工智能等各个方面,感觉是一个比较全的资源.供参考! 原文的出处:http://www.codemonsters.de/ho ...
- sql 语言--- DML,DDL,DQL,DCL,TCL,CCL
结构化查询语言(Structured Query Language)简称SQL 是一种特殊目的的编程语言,是一种数据库查询和程序设计语言,用于存取数据以及查询.更新和管理 ...
- iphone如何安装mitmproxy的pem文件(当iphone无法使用邮箱时)
背景描述: 我要安装mitmproxy描述文件 mitmproxy-ca-cert.pem , 说是用iphone自带的邮箱接收然后安装即可,但悲剧的邮箱坏了[提示:无法连接服务器],查了不少方法都无 ...