思考(作业):基于一个数据结构做缓存,怎么实现LRU——最长时间不被访问的元素在超过容量时删除?
问题:如果基于传统LRU 算法实现Redis LRU 会有什么问题?
需要额外的数据结构存储,消耗内存。
Redis LRU 对传统的LRU 算法进行了改良,通过随机采样来调整算法的精度。
如果淘汰策略是LRU,则根据配置的采样值maxmemory_samples(默认是5 个),随机从数据库中选择m 个key, 淘汰其中热度最低的key 对应的缓存数据。所以采样参数m 配置的数值越大, 就越能精确的查找到待淘汰的缓存数据,但是也消耗更多的CPU 计算,执行效率降低。
问题:如何找出热度最低的数据?
Redis 中所有对象结构都有一个lru 字段, 且使用了unsigned 的低24 位,这个字段用来记录对象的热度。对象被创建时会记录lru 值。在被访问的时候也会更新lru 的值。但是不是获取系统当前的时间戳,而是设置为全局变量server.lruclock 的值。
源码:server.h
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;
server.lruclock 的值怎么来的?
Redis 中有个定时处理的函数serverCron , 默认每100 毫秒调用函数updateCachedTime 更新一次全局变量的server.lruclock 的值,它记录的是当前unix时间戳。
源码:server.c
void updateCachedTime(void) {
time_t unixtime = time(NULL);
atomicSet(server.unixtime,unixtime);
server.mstime = mstime();
struct tm tm;
localtime_r(&server.unixtime,&tm);
server.daylight_active = tm.tm_isdst;
}
问题:为什么不获取精确的时间而是放在全局变量中?不会有延迟的问题吗?
这样函数lookupKey 中更新数据的lru 热度值时,就不用每次调用系统函数time,可以提高执行效率。
OK,当对象里面已经有了LRU 字段的值,就可以评估对象的热度了。
函数estimateObjectIdleTime 评估指定对象的lru 热度,思想就是对象的lru 值和全局的server.lruclock 的差值越大(越久没有得到更新), 该对象热度越低。
源码evict.c
/* Given an object returns the min number of milliseconds the object was never
* requested, using an approximated LRU algorithm. */
unsigned long long estimateObjectIdleTime(robj *o) {
unsigned long long lruclock = LRU_CLOCK();
if (lruclock >= o->lru) {
return (lruclock - o->lru) * LRU_CLOCK_RESOLUTION;
} else {
return (lruclock + (LRU_CLOCK_MAX - o->lru)) *
LRU_CLOCK_RESOLUTION;
}
}
server.lruclock 只有24 位,按秒为单位来表示才能存储194 天。当超过24bit 能表示的最大时间的时候,它会从头开始计算。
server.h
#define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) /* Max value of obj->lru */
在这种情况下,可能会出现对象的lru 大于server.lruclock 的情况,如果这种情况出现那么就两个相加而不是相减来求最久的key。
为什么不用常规的哈希表+双向链表的方式实现?需要额外的数据结构,消耗资源。而Redis LRU 算法在sample 为10 的情况下,已经能接近传统LRU 算法了。
https://redis.io/topics/lru-cache
问题:除了消耗资源之外,传统LRU 还有什么问题?
如图,假设A 在10 秒内被访问了5 次,而B 在10 秒内被访问了3 次。因为B 最后一次被访问的时间比A 要晚,在同等的情况下,A 反而先被回收。
问题:要实现基于访问频率的淘汰机制,怎么做?
LFU
server.h
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;
当这24 bits 用作LFU 时,其被分为两部分:
高16 位用来记录访问时间(单位为分钟,ldt,last decrement time)
低8 位用来记录访问频率,简称counter(logc,logistic counter)
counter 是用基于概率的对数计数器实现的,8 位可以表示百万次的访问频率。
对象被读写的时候,lfu 的值会被更新。
db.c——lookupKey
void updateLFU(robj *val) {
unsigned long counter = LFUDecrAndReturn(val);
counter = LFULogIncr(counter);
val->lru = (LFUGetTimeInMinutes()<<8) | counter;
}
增长的速率由,lfu-log-factor 越大,counter 增长的越慢
redis.conf 配置文件
# lfu-log-factor 10
如果计数器只会递增不会递减,也不能体现对象的热度。没有被访问的时候,计数器怎么递减呢?
减少的值由衰减因子lfu-decay-time(分钟)来控制,如果值是1 的话,N 分钟没有访问就要减少N。
redis.conf 配置文件
# lfu-decay-time 1