redis的過時淘汰策略是很是值得去深刻了解以及考究的一個問題。不少使用者每每不能深得其意,每每停留在人云亦云的程度,若生產不出事故便划水就划過去了,可是當生產數據莫名其妙的消失,或者reids服務崩潰的時候,卻又一籌莫展。本文嘗試着從淺入深的將redis的過時策略剖析開來,指望幫助做者以及讀者站在一個更加系統化的角度去看待過時策略。redis
redis做爲緩存數據庫,其底層數據結構主要由dict和expires兩個字典構成,其中dict字典負責保存鍵值對,而expires字典則負責保存鍵的過時時間。訪問磁盤空間的成本是訪問緩存的成本高出很是多,因此內存的成本比磁盤空間要大。在實際使用中,緩存的空間每每極爲有限,因此爲了在爲數很少的容量中作到真正的物盡其用,必需要對緩存的容量進行管控。算法
redis經過配置
maxmemory
來配置最大容量(閾值),當數據佔有空間超過所設定值就會觸發內部的內存淘汰策略(內存釋放)。那麼究竟要淘汰哪些數據,纔是最符合業務需求?或者在業務容忍的範圍內呢?爲了解決這個問題,redis提供了可配置的淘汰策略,讓使用者能夠配置適合本身業務場景的淘汰策略,不配置的狀況下默認是使用volatile-lru
。數據庫
若是redis保存的key-value對數量很少(好比數十對),那麼當內存超過閾值後,對整個內存空間的全部key進行檢查,也無傷大雅。然而,在實際使用中redis保存的key-value數量遠遠不止於此,若是使用內存超過閾值就逐個去檢查是否符合過時策略嗎?隨意遍歷十萬個key?顯然不是,不然,redis又何以高性能著稱?可是,檢查多少key這個問題確實存在。爲了解決該問題,redis的設計者們引入一個配置項maxmemory-samples
,稱之爲過時檢測樣本,默認值是3,經過它來曲線救國。緩存
過時檢測樣本是如何配合redis來進行數據清理呢?服務器
當mem_used
內存已經超過maxmemory
的設定,對於全部的讀寫請求,都會觸發redis.c/freeMemoryIfNeeded
函數以清理超出的內存。注意這個清理過程是阻塞的
,直到清理出足夠的內存空間。因此若是在達到maxmemory
而且調用方還在不斷寫入的狀況下,可能會反覆觸發主動清理策略,致使請求會有必定的延遲。數據結構
清理時會根據用戶配置的maxmemory
政策來作適當的清理(通常是LRU或TTL),這裏的LRU或TTL策略並非針對redis的的全部鍵,而是以配置文件中的maxmemory
樣本個鍵做爲樣本池進行抽樣清理。app
redis設計者將該值默認爲3,若是增長該值,會提升LRU或TTL的精準度,redis的做者測試的結果是當這個配置爲10時已經很是接近全量LRU的精準度了,並且增長
maxmemory
採樣會致使在主動清理時消耗更多的CPU時間,因此在設置該值必須慎重把控,在業務的需求以及性能之間作權衡。建議以下:dom
maxmemory
,最好在mem_used
內存佔用達到maxmemory
的必定比例後,須要考慮調大赫茲以加快淘汰,或者進行集羣擴容。maxmemory-samples
配置。若是redis自己就做爲LRU緩存服務(這種服務通常長時間處於maxmemory
狀態,由redis自動作LRU淘汰),能夠適當調大maxmemory
樣本。int freeMemoryIfNeeded(void) { size_t mem_used, mem_tofree, mem_freed; int slaves = listLength(server.slaves); // 計算佔用內存大小時,並不計算slave output buffer和aof buffer, // 所以maxmemory應該比實際內存小,爲這兩個buffer留足空間。 mem_used = zmalloc_used_memory(); 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; } } if (server.appendonly) { mem_used -= sdslen(server.aofbuf); mem_used -= sdslen(server.bgrewritebuf); } // 判斷已經使用內存是否超過最大使用內存,若是沒有超過就返回REDIS_OK, if (mem_used <= server.maxmemory) return REDIS_OK; // 當超過了最大使用內存時,就要判斷此時redis到底採用何種內存釋放策略,根據不一樣的策略,採起不一樣的清除算法。 // 首先判斷是不是爲no-enviction策略,若是是,則返回REDIS_ERR,而後redis就再也不接受任何寫命令了。 if (server.maxmemory_policy == REDIS_MAXMEMORY_NO_EVICTION) return REDIS_ERR; // 計算須要清理內存大小 mem_tofree = mem_used - server.maxmemory; mem_freed = 0; while (mem_freed < mem_tofree) { int j, k, keys_freed = 0; for (j = 0; j < server.dbnum; j++) { long bestval = 0; sds bestkey = NULL; struct dictEntry *de; redisDb *db = server.db+j; dict *dict; // 一、從哪一個字典中剔除數據 // 判斷淘汰策略是基於全部的鍵仍是隻是基於設置了過時時間的鍵, // 若是是針對全部的鍵,就從server.db[j].dict中取數據, // 若是是針對設置了過時時間的鍵,就從server.db[j].expires(記錄過時時間)中取數據。 if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU || server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM) { dict = server.db[j].dict; } else { dict = server.db[j].expires; } if (dictSize(dict) == 0) continue; // 二、從是否爲隨機策略 // 是否是random策略,包括volatile-random 和allkeys-random,這兩種策略是最簡單的,就是在上面的數據集中隨便去一個鍵,而後刪掉。 if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM || server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_RANDOM) { de = dictGetRandomKey(dict);// 從方法名猜出是隨機獲取一個dictEntry bestkey = dictGetEntryKey(de);// 獲得刪除的key } // 三、判斷是否爲lru算法 // 是lru策略仍是ttl策略,若是是lru策略就採用lru近似算法 // 爲了減小運算量,redis的lru算法和expire淘汰算法同樣,都是非最優解, // lru算法是在相應的dict中,選擇maxmemory_samples(默認設置是3)份key,挑選其中lru的,進行淘汰 else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU || server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU) { for (k = 0; k < server.maxmemory_samples; k++) { sds thiskey; long thisval; robj *o; de = dictGetRandomKey(dict); thiskey = dictGetEntryKey(de); /* When policy is volatile-lru we need an additonal lookup * to locate the real key, as dict is set to db->expires. */ if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU) de = dictFind(db->dict, thiskey); //由於dict->expires維護的數據結構裏並無記錄該key的最後訪問時間 o = dictGetEntryVal(de); thisval = estimateObjectIdleTime(o); /* Higher idle time is better candidate for deletion */ // 找到那個最合適刪除的key // 相似排序,循環後找到最近最少使用,將其刪除 if (bestkey == NULL || thisval > bestval) { bestkey = thiskey; bestval = thisval; } } } // 若是是ttl策略。 // 取maxmemory_samples個鍵,比較過時時間, // 從這些鍵中找到最快過時的那個鍵,並將其刪除 else if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_TTL) { for (k = 0; k < server.maxmemory_samples; k++) { sds thiskey; long thisval; de = dictGetRandomKey(dict); thiskey = dictGetEntryKey(de); thisval = (long) dictGetEntryVal(de); /* Expire sooner (minor expire unix timestamp) is better candidate for deletion */ if (bestkey == NULL || thisval < bestval) { bestkey = thiskey; bestval = thisval; } } } // 根據不一樣策略挑選了即將刪除的key以後,進行刪除 if (bestkey) { long long delta; robj *keyobj = createStringObject(bestkey,sdslen(bestkey)); // 發佈數據更新消息,主要是AOF 持久化和從機 propagateExpire(db,keyobj); //將del命令擴散給slaves // 注意, propagateExpire() 可能會致使內存的分配, // propagateExpire() 提早執行就是由於redis 只計算 // dbDelete() 釋放的內存大小。假若同時計算dbDelete() // 釋放的內存和propagateExpire() 分配空間的大小,與此 // 同時假設分配空間大於釋放空間,就有可能永遠退不出這個循環。 // 下面的代碼會同時計算dbDelete() 釋放的內存和propagateExpire() 分配空間的大小 /* We compute the amount of memory freed by dbDelete() alone. * It is possible that actually the memory needed to propagate * the DEL in AOF and replication link is greater than the one * we are freeing removing the key, but we can't account for * that otherwise we would never exit the loop. * * AOF and Output buffer memory will be freed eventually so * we only care about memory used by the key space. */ // 只計算dbDelete() 釋放內存的大小 delta = (long long) zmalloc_used_memory(); dbDelete(db,keyobj); delta -= (long long) zmalloc_used_memory(); mem_freed += delta; server.stat_evictedkeys++; decrRefCount(keyobj); keys_freed++; /* 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(); } }//在全部的db中遍歷一遍,而後判斷刪除的key釋放的空間是否足夠,未能釋放空間,且此時redis 使用的內存大小依舊超額,失敗返回 if (!keys_freed) return REDIS_ERR; /* nothing to free... */ } return REDIS_OK; }
從源碼分析中能夠看到redis在使用內存中超過設定的閾值時是如何將清理key-value進行內管管理,其中涉及到redis的存儲結構。開篇就說到redis底層數據結構是由dict以及expires兩個字典構成,經過一張圖能夠很是清晰瞭解到redis中帶過時時間的key-value的存儲結構,能夠更加深入認識到redis的內存管理機制。ide
從redis的內存管理機制中咱們能夠看到,當使用的內存超過設定的閾值,就會觸發內存清理。那麼必定要等到內存超過閾值才進行內存清理嗎?非要亡羊補牢?redis的設計者顯然是考慮到了這個問題,當redis在使用過程當中,自行去刪除一些過時key,儘可能保證不要觸發超過內存閾值而發生的清理事件。函數
在使用過時時間時,必要注意以下三點:
一、定時刪除(主動刪除策略):經過使用定時器(時間事件,採用無序鏈表實現,),定時刪除數據。定時刪除策略能夠保證過時的鍵會盡量快的被刪除了,並釋放過時鍵鎖佔用的內存。
二、惰性刪除(被動刪除策略):程序在每次使用到鍵的時候去檢查是否過時,若是過時則刪除並返回空。
三、按期刪除(主動刪除):按期每隔一段時間執行一段刪除過時鍵操做,經過限制刪除操做的執行時長與頻率來減小刪除操做對CPU時間的影響。除此以外,按期執行也能夠減小過時鍵長期駐留內存的影響,減小內存泄漏的可能。
redis服務器實際上使用的惰性刪除和按期刪除兩種策略:經過配合使用兩種刪除策略,服務器能夠很好地合理使用CPU時間和避免浪費內存空間之間取得平衡。
redis提供一個
expireIfNeeded
函數,因此讀寫數據庫的命令在執行以前都必須調用expireIfNeeded函數。(鍵是否存在)
按期刪除有函數
activeExpireCycle
函數實現,每當redis服務器調用serverCorn
函數時執行按期刪除函數。它會在規定時間
內,分屢次遍歷服務器中的各個數據庫,並在數據庫的expire字典中隨機檢查
一部分鍵的過時時間,並刪除過時鍵。
遍歷數據庫(就是redis.conf中配置的"database"數量,默認爲16)