本文講的是 當(dāng)redis設(shè)定了最大內(nèi)存之后,緩存中的數(shù)據(jù)集大小超過了一定比例,實施的淘汰策略,不是刪除過期鍵的策略,雖然兩者非常相似。
在 redis 中,允許用戶設(shè)置最大使用內(nèi)存大小通過配置redis.conf中的maxmemory這個值來開啟內(nèi)存淘汰功能,在內(nèi)存限定的情況下是很有用的。
設(shè)置最大內(nèi)存大小可以保證redis對外提供穩(wěn)健服務(wù)。
推薦:redis教程
redis 內(nèi)存數(shù)據(jù)集大小上升到一定大小的時候,就會施行數(shù)據(jù)淘汰策略。redis 提供 6種數(shù)據(jù)淘汰策略通過maxmemory-policy設(shè)置策略:
volatile-lru:從已設(shè)置過期時間的數(shù)據(jù)集(server.db[i].expires)中挑選最近最少使用的數(shù)據(jù)淘汰
volatile-ttl:從已設(shè)置過期時間的數(shù)據(jù)集(server.db[i].expires)中挑選將要過期的數(shù)據(jù)淘汰
volatile-random:從已設(shè)置過期時間的數(shù)據(jù)集(server.db[i].expires)中任意選擇數(shù)據(jù)淘汰
allkeys-lru:從數(shù)據(jù)集(server.db[i].dict)中挑選最近最少使用的數(shù)據(jù)淘汰
allkeys-random:從數(shù)據(jù)集(server.db[i].dict)中任意選擇數(shù)據(jù)淘汰
no-enviction(驅(qū)逐):禁止驅(qū)逐數(shù)據(jù)
redis 確定驅(qū)逐某個鍵值對后,會刪除這個數(shù)據(jù)并將這個數(shù)據(jù)變更消息發(fā)布到本地(AOF 持久化)和從機(jī)(主從連接)
LRU 數(shù)據(jù)淘汰機(jī)制
在服務(wù)器配置中保存了 lru 計數(shù)器 server.lrulock,會定時(redis 定時程序 serverCorn())更新,server.lrulock 的值是根據(jù) server.unixtime 計算出來的。
另外,從 Struct redisObject 中可以發(fā)現(xiàn),每一個 redis 對象都會設(shè)置相應(yīng)的 lru??梢韵胂蟮氖?,每一次訪問數(shù)據(jù)的時候,會更新 redisObject.lru。
LRU 數(shù)據(jù)淘汰機(jī)制是這樣的:在數(shù)據(jù)集中隨機(jī)挑選幾個鍵值對,取出其中 lru 最大的鍵值對淘汰。所以,你會發(fā)現(xiàn),redis 并不是保證取得所有數(shù)據(jù)集中最近最少使用(LRU)的鍵值對,而只是隨機(jī)挑選的幾個鍵值對中的。
//?redisServer?保存了?lru?計數(shù)器 struct?redisServer?{ ... unsigned?lruclock:22;?/*?Clock?incrementing?every?minute,?for?LRU?*/ ... }; ? //?每一個?redis?對象都保存了?lru #define?REDIS_LRU_CLOCK_MAX?((1lru?*/ #define?REDIS_LRU_CLOCK_RESOLUTION?10?/*?LRU?clock?resolution?in?seconds?*/ typedef?struct?redisObject?{ //?剛剛好?32?bits ? //?對象的類型,字符串/列表/集合/哈希表 unsigned?type:4; //?未使用的兩個位 unsigned?notused:2;?/*?Not?used?*/ //?編碼的方式,redis?為了節(jié)省空間,提供多種方式來保存一個數(shù)據(jù) //?譬如:“123456789”?會被存儲為整數(shù)?123456789 unsigned?encoding:4; unsigned?lru:22;?/*?lru?time?(relative?to?server.lruclock)?*/ ? //?引用數(shù) int?refcount; ? //?數(shù)據(jù)指針 void?*ptr; }?robj; ? //?redis?定時執(zhí)行程序。聯(lián)想:linux?cron int?serverCron(struct?aeEventLoop?*eventLoop,?long?long?id,?void?*clientData)?{ ...... /*?We?have?just?22?bits?per?object?for?LRU?information. *?So?we?use?an?(eventually?wrapping)?LRU?clock?with?10?seconds?resolution. *?2^22?bits?with?10?seconds?resolution?is?more?or?less?1.5?years. * *?Note?that?even?if?this?will?wrap?after?1.5?years?it's?not?a?problem, *?everything?will?still?work?but?just?some?object?will?appear?younger *?to?Redis.?But?for?this?to?happen?a?given?object?should?never?be?touched *?for?1.5?years. * *?Note?that?you?can?change?the?resolution?altering?the *?REDIS_LRU_CLOCK_RESOLUTION?define. */ updateLRUClock(); ...... } ? //?更新服務(wù)器的?lru?計數(shù)器 void?updateLRUClock(void)?{ server.lruclock?=?(server.unixtime/REDIS_LRU_CLOCK_RESOLUTION)?& REDIS_LRU_CLOCK_MAX; }
TTL 數(shù)據(jù)淘汰機(jī)制
redis 數(shù)據(jù)集數(shù)據(jù)結(jié)構(gòu)中保存了鍵值對過期時間的表,即 redisDb.expires。和 LRU 數(shù)據(jù)淘汰機(jī)制類似,TTL 數(shù)據(jù)淘汰機(jī)制是這樣的:從過期時間的表中隨機(jī)挑選幾個鍵值對,取出其中 ttl 最大的鍵值對淘汰。同樣你會發(fā)現(xiàn),redis 并不是保證取得所有過期時間的表中最快過期的鍵值對,而只是隨機(jī)挑選的幾個鍵值對中的。
總結(jié)
redis 每服務(wù)客戶端執(zhí)行一個命令的時候,會檢測使用的內(nèi)存是否超額。如果超額,即進(jìn)行數(shù)據(jù)淘汰。
//?執(zhí)行命令 int?processCommand(redisClient?*c)?{ ...... //?內(nèi)存超額 /*?Handle?the?maxmemory?directive. * *?First?we?try?to?free?some?memory?if?possible?(if?there?are?volatile *?keys?in?the?dataset).?If?there?are?not?the?only?thing?we?can?do *?is?returning?an?error.?*/ if?(server.maxmemory)?{ int?retval?=?freeMemoryIfNeeded(); if?((c->cmd->flags?&?REDIS_CMD_DENYOOM)?&&?retval?==?REDIS_ERR)?{ flagTransaction(c); addReply(c,?shared.oomerr); return?REDIS_OK; } } ...... } ? //?如果需要,是否一些內(nèi)存 int?freeMemoryIfNeeded(void)?{ size_t?mem_used,?mem_tofree,?mem_freed; int?slaves?=?listLength(server.slaves); ? //?redis?從機(jī)回復(fù)空間和?AOF?內(nèi)存大小不計算入?redis?內(nèi)存大小 /*?Remove?the?size?of?slaves?output?buffers?and?AOF?buffer?from?the *?count?of?used?memory.?*/ mem_used?=?zmalloc_used_memory(); ? //?從機(jī)回復(fù)空間大小 if?(slaves)?{ listIter?li; listNode?*ln; ? listRewind(server.slaves,&li); while((ln?=?listNext(&li)))?{ redisClient?*slave?=?listNodeValue(ln); unsigned?long?obuf_bytes?=?getClientOutputBufferMemoryUsage(slave); if?(obuf_bytes?>?mem_used) mem_used?=?0; else mem_used?-=?obuf_bytes; } } //?server.aof_buf?&&?server.aof_rewrite_buf_blocks if?(server.aof_state?!=?REDIS_AOF_OFF)?{ mem_used?-=?sdslen(server.aof_buf); mem_used?-=?aofRewriteBufferSize(); } ? //?內(nèi)存是否超過設(shè)置大小 /*?Check?if?we?are?over?the?memory?limit.?*/ if?(mem_used?expires.?*/ if?(server.maxmemory_policy?==?REDIS_MAXMEMORY_VOLATILE_LRU) de?=?dictFind(db->dict,?thiskey); o?=?dictGetVal(de); ? //?計算數(shù)據(jù)的空閑時間 thisval?=?estimateObjectIdleTime(o); ? //?當(dāng)前鍵值空閑時間更長,則記錄 /*?Higher?idle?time?is?better?candidate?for?deletion?*/ if?(bestkey?==?NULL?||?thisval?>?bestval)?{ bestkey?=?thiskey; bestval?=?thisval; } } } ? //?TTL?策略:挑選將要過期的數(shù)據(jù) /*?volatile-ttl?*/ else?if?(server.maxmemory_policy?==?REDIS_MAXMEMORY_VOLATILE_TTL)?{ //?server.maxmemory_samples?為隨機(jī)挑選鍵值對次數(shù) //?隨機(jī)挑選?server.maxmemory_samples個鍵值對,驅(qū)逐最快要過期的數(shù)據(jù) for?(k?=?0;?k?id); decrRefCount(keyobj); keys_freed++; ? //?將從機(jī)回復(fù)空間中的數(shù)據(jù)及時發(fā)送給從機(jī) /*?When?the?memory?to?free?starts?to?be?big?enough,?we?may *?start?spending?so?much?time?here?that?is?impossible?to *?deliver?data?to?the?slaves?fast?enough,?so?we?force?the *?transmission?here?inside?the?loop.?*/ if?(slaves)?flushSlavesOutputBuffers(); } } ? //?未能釋放空間,且此時?redis?使用的內(nèi)存大小依舊超額,失敗返回 if?(!keys_freed)?return?REDIS_ERR;?/*?nothing?to?free...?*/ } return?REDIS_OK; }
適用場景
下面看看幾種策略的適用場景:
allkeys-lru: 如果我們的應(yīng)用對緩存的訪問符合冪律分布(也就是存在相對熱點數(shù)據(jù)),或者我們不太清楚我們應(yīng)用的緩存訪問分布狀況,我們可以選擇allkeys-lru策略。
allkeys-random: 如果我們的應(yīng)用對于緩存key的訪問概率相等,則可以使用這個策略。
volatile-ttl: 這種策略使得我們可以向Redis提示哪些key更適合被eviction。
另外,volatile-lru策略和volatile-random策略適合我們將一個Redis實例既應(yīng)用于緩存和又應(yīng)用于持久化存儲的時候,然而我們也可以通過使用兩個Redis實例來達(dá)到相同的效果,值得一提的是將key設(shè)置過期時間實際上會消耗更多的內(nèi)存,因此我們建議使用allkeys-lru策略從而更有效率的使用內(nèi)存。