Redis LRU 淘汰原理

思考(作业):基于一个数据结构做缓存,怎么实现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

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值