在redis的lru的实现与传统的lru实现不同。
具体实现在evict.c文件中,当redis需要通过释放缓存的key来释放空间时,将会通过ecict.c的freeMemoryIfNeeded()函数来通过设定的算法来清除key以腾出空间。
其中,如果配置的策略是ALL_KEYS才会从所有缓存的key尝试释放,否则只会从存在过期时间的key中进行释放。
struct evictionPoolEntry *pool = EvictionPoolLRU;
首先,当采用lru或者lfu的时候,将会得到evict池pool,其实也就是一个evictionPoolEntry数组来准备将清除的键放入,池中具体的结构如下。
struct evictionPoolEntry {unsigned long long idle; /* Object idle time (inverse frequency for LFU) */sds key;/* Key name. */sds cached; /* Cached SDS object for key name. */int dbid; /* Key DB number. */};
其中主要用到的是long long类型的idle,来判断是否需要被释放的依据。
for (i = 0; i < server.dbnum; i++) {db = server.db+i;dict = (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) ?db->dict : db->expires;if ((keys = dictSize(dict)) != 0) {evictionPoolPopulate(i, dict, db->dict, pool);total_keys += keys;}}
之后将会从一次从所有db中依次根据算法来得到清除的key,具体操作在evictionPoolPopulate()函数中。
当采用lru时,redisObject中通过一个24位大小的字段来存放key最后一次被访问的时间。
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;
在evictionPoolPopulate()函数中,首先会在对应的db中随机获取count个数的key,之后会依次获取当前redis的lru时钟距离这个key最后一次被调用所隔时间。
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;}}
在获得了随机获取的key之后,将会通过插入排序的方式,根据当前时间距离这个key最后一次访问时间的大小,来插入到evict池中,而经过所有db依次重复上述的操作,evict池中留下的将是所有db中随机获取的key中最长时间没有被访问的,将会被清除,这是redis的lru实现。
k = 0;while (k < EVPOOL_SIZE &&pool[k].key &&pool[k].idle < idle) k++;if (k == 0 && pool[EVPOOL_SIZE-1].key != NULL) {/* Can't insert if the element is < the worst element we have* and there are no empty buckets. */continue;} else if (k < EVPOOL_SIZE && pool[k].key == NULL) {/* Inserting into empty position. No setup needed before insert. */} else {/* Inserting in the middle. Now k points to the first element* greater than the element to insert. */if (pool[EVPOOL_SIZE-1].key == NULL) {/* Free space on the right? Insert at k shifting* all the elements from k to end to the right. *//* Save SDS before overwriting. */sds cached = pool[EVPOOL_SIZE-1].cached;memmove(pool+k+1,pool+k,sizeof(pool[0])*(EVPOOL_SIZE-k-1));pool[k].cached = cached;} else {/* No free space on right? Insert at k-1 */k--;/* Shift all elements on the left of k (included) to the* left, so we discard the element with smaller idle time. */sds cached = pool[0].cached; /* Save SDS before overwriting. */if (pool[0].key != pool[0].cached) sdsfree(pool[0].key);memmove(pool,pool+1,sizeof(pool[0])*k);pool[k].cached = cached;}}
在lru的基础上,lfu做出了改进。
Lru只是单纯通过idle记录了当前时间距离最后一次访问到的时间,然而也仅仅通过这个时间来判断了,缺少对于频率这一属性的支持,lfu在lru做出了相应的改进。
同样是之前的24位的lru字段,在lfu算法下,前16位将会存放最后一次访问的时间,精确到分钟,而后八位将会记录一个couter频率值,作为判定的依据。
以下是在访问key是更新频率的函数LFULogIncr()。
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;}
频率的更新依赖于随机数,也就是概率,LFU_INIT_VAL值为5,server.lfu_log_factor默认为10也就是说,默认情况下couter是否加一,依赖于1/((counter - 5)*10+1)的结果。当couter越小,其结果也越大,其大于一个随机数的概率也越大,couter的频率增长速度也越快,反之其正常速度也越慢,因此可以通过这个值来形容频率。
而另外16位,则是当在获取couter来进行比较的时候,根据最后一次访问时间来确定是不是过长时间没有访问而需要将频率减少。
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;}unsigned long LFUTimeElapsed(unsigned long ldt) {unsigned long now = LFUGetTimeInMinutes();if (now >= ldt) return now-ldt;return 65535-ldt+now;}
当正式获取lfu的数据的时候,将会把当前时间与最后访问时间的差除以衰减因素来确定要减去的couter,couter减去结果并返回,作为最后的判断指标,越小将越有可能被清除缓存。
因此在lfu下,不仅会简单的清除最长时间没有被访问的,将会引入频率的概念,清除两者综合下来的结果,达到更合理的目的。
如果觉得《redis lru和lfu的实现》对你有帮助,请点赞、收藏,并留下你的观点哦!