前言
在Redis中的LRU算法文中說(shuō)到,LRU有一個(gè)缺陷,在如下情況下:
~~~~~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|
會(huì)將數(shù)據(jù)D誤認(rèn)為將來(lái)最有可能被訪問(wèn)到的數(shù)據(jù)。
Redis作者曾想改進(jìn)LRU算法,但發(fā)現(xiàn)Redis的LRU算法受制于隨機(jī)采樣數(shù)maxmemory_samples,在maxmemory_samples等于10的情況下已經(jīng)很接近于理想的LRU算法性能,也就是說(shuō),LRU算法本身已經(jīng)很難再進(jìn)一步了。
于是,將思路回到原點(diǎn),淘汰算法的本意是保留那些將來(lái)最有可能被再次訪問(wèn)的數(shù)據(jù),而LRU算法只是預(yù)測(cè)最近被訪問(wèn)的數(shù)據(jù)將來(lái)最有可能被訪問(wèn)到。我們可以轉(zhuǎn)變思路,采用一種LFU(Least Frequently Used)算法,也就是最頻繁被訪問(wèn)的數(shù)據(jù)將來(lái)最有可能被訪問(wèn)到。在上面的情況中,根據(jù)訪問(wèn)頻繁情況,可以確定保留優(yōu)先級(jí):B>A>C=D。
Redis中的LFU思路
在LFU算法中,可以為每個(gè)key維護(hù)一個(gè)計(jì)數(shù)器。每次key被訪問(wèn)的時(shí)候,計(jì)數(shù)器增大。計(jì)數(shù)器越大,可以約等于訪問(wèn)越頻繁。
上述簡(jiǎn)單算法存在兩個(gè)問(wèn)題:
第一個(gè)問(wèn)題很好解決,可以借鑒LRU實(shí)現(xiàn)的經(jīng)驗(yàn),維護(hù)一個(gè)待淘汰key的pool。第二個(gè)問(wèn)題的解決辦法是,記錄key最后一個(gè)被訪問(wèn)的時(shí)間,然后隨著時(shí)間推移,降低計(jì)數(shù)器。
Redis對(duì)象的結(jié)構(gòu)如下:
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是用來(lái)記錄LRU time的,在LFU中也可以使用這個(gè)字段,不過(guò)是分成16 bits與8 bits使用:
16 bits 8 bits +----------------+--------+ + Last decr time | LOG_C | +----------------+--------+
高16 bits用來(lái)記錄最近一次計(jì)數(shù)器降低的時(shí)間ldt,單位是分鐘,低8 bits記錄計(jì)數(shù)器數(shù)值counter。
LFU配置
Redis4.0之后為maxmemory_policy淘汰策略添加了兩個(gè)LFU模式:
還有2個(gè)配置可以調(diào)整LFU算法:
lfu-log-factor 10 lfu-decay-time 1
lfu-log-factor可以調(diào)整計(jì)數(shù)器counter的增長(zhǎng)速度,lfu-log-factor越大,counter增長(zhǎng)的越慢。
lfu-decay-time是一個(gè)以分鐘為單位的數(shù)值,可以調(diào)整counter的減少速度
源碼實(shí)現(xiàn)
在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; } }
當(dāng)采用LFU策略時(shí),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對(duì)counter進(jìn)行減少操作:
/* 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; }
函數(shù)首先取得高16 bits的最近降低時(shí)間ldt與低8 bits的計(jì)數(shù)器counter,然后根據(jù)配置的lfu_decay_time計(jì)算應(yīng)該降低多少。
LFUTimeElapsed用來(lái)計(jì)算當(dāng)前時(shí)間與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; }
具體是當(dāng)前時(shí)間轉(zhuǎn)化成分鐘數(shù)后取低16 bits,然后計(jì)算與ldt的差值now-ldt。當(dāng)ldt > now時(shí),默認(rèn)為過(guò)了一個(gè)周期(16 bits,最大65535),取值65535-ldt+now。
然后用差值與配置lfu_decay_time相除,LFUTimeElapsed(ldt) / server.lfu_decay_time,已過(guò)去n個(gè)lfu_decay_time,則將counter減少n,counter - num_periods。
增長(zhǎng)LFULogIncr
增長(zhǎng)函數(shù)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并不是簡(jiǎn)單的訪問(wèn)一次就+1,而是采用了一個(gè)0-1之間的p因子控制增長(zhǎng)。counter最大值為255。取一個(gè)0-1之間的隨機(jī)數(shù)r與p比較,當(dāng)rp時(shí),才增加counter,這和比特幣中控制產(chǎn)出的策略類(lèi)似。p取決于當(dāng)前counter值與lfu_log_factor因子,counter值與lfu_log_factor因子越大,p越小,rp的概率也越小,counter增長(zhǎng)的概率也就越小。增長(zhǎng)情況如下:
+--------+------------+------------+------------+------------+------------+
| 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 |
+--------+------------+------------+------------+------------+------------+
可見(jiàn)counter增長(zhǎng)與訪問(wèn)次數(shù)呈現(xiàn)對(duì)數(shù)增長(zhǎng)的趨勢(shì),隨著訪問(wèn)次數(shù)越來(lái)越大,counter增長(zhǎng)的越來(lái)越慢。
新生key策略
另外一個(gè)問(wèn)題是,當(dāng)創(chuàng)建新對(duì)象的時(shí)候,對(duì)象的counter如果為0,很容易就會(huì)被淘汰掉,還需要為新生key設(shè)置一個(gè)初始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會(huì)被初始化為L(zhǎng)FU_INIT_VAL,默認(rèn)5。
pool
pool算法就與LRU算法一致了:
if (server.maxmemory_policy (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU) || server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL)
計(jì)算idle時(shí)有所不同:
} 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)當(dāng)做排序的依據(jù)。
參考鏈接
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)腳本之家的支持。
標(biāo)簽:伊春 南寧 定州 河源 泰州 拉薩 甘南 畢節(jié)
巨人網(wǎng)絡(luò)通訊聲明:本文標(biāo)題《Redis中LFU算法的深入分析》,本文關(guān)鍵詞 Redis,中,LFU,算法,的,深入分析,;如發(fā)現(xiàn)本文內(nèi)容存在版權(quán)問(wèn)題,煩請(qǐng)?zhí)峁┫嚓P(guān)信息告之我們,我們將及時(shí)溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡(luò),涉及言論、版權(quán)與本站無(wú)關(guān)。下一篇:Redis字符串原理的深入理解