深入理解Redis內(nèi)存回收和內(nèi)存淘汰機(jī)制
1 概念
Redis 所有的數(shù)據(jù)都是存儲(chǔ)在內(nèi)存中的, 如果不進(jìn)行任何的內(nèi)存回收, 那么很容易出現(xiàn)內(nèi)存爆滿的情況。因此,在某些情況下需要對(duì)占用的內(nèi)存空間進(jìn)行釋放。
Redis 中內(nèi)存的釋放主要分為兩類(lèi)
Redis 中內(nèi)存的釋放主要分為兩類(lèi):
內(nèi)存回收: 將過(guò)期的 key 清除,以減少內(nèi)存占用
內(nèi)存淘汰: 在內(nèi)存使用達(dá)到上限(max_memory), 按照一定的策略刪除一些鍵,以釋放內(nèi)存空間
兩者都是通過(guò)刪除 key (及其對(duì)應(yīng)的 value) 來(lái)達(dá)到釋放空間的效果。
區(qū)別在于前者清除的是用戶明確不需要的 key, 而后者清除的則是用戶可能仍然需要的 key。
2 內(nèi)存回收
2.1 過(guò)期策略
在內(nèi)存中的大量 key 中, 如何清除其中已經(jīng)過(guò)期的 key 呢?
常用的方式有 3 種
- 定時(shí)過(guò)期
- 惰性過(guò)期
- 定期過(guò)期
定時(shí)過(guò)期
為每個(gè) key 都創(chuàng)建一個(gè)定時(shí)器, 時(shí)間到了, 就將這個(gè) key 清除。
該策略可以立即清除過(guò)期的數(shù)據(jù), 對(duì)內(nèi)存很友好。但是會(huì)占用大量的 CPU 資源去處理過(guò)期的數(shù)據(jù), 從而影響緩存的響應(yīng)時(shí)間和吞吐量。
惰性過(guò)期
key 過(guò)期了, 不進(jìn)行處理。當(dāng)后續(xù)訪問(wèn)到這個(gè) key 時(shí), 才會(huì)判斷該 key 是否已過(guò)期, 過(guò)期則清除。
該策略可以最大化地節(jié)省 CPU 資源, 卻對(duì)內(nèi)存非常不友好。極端情況可能出現(xiàn)大量的過(guò)期 key 沒(méi)有再次被訪問(wèn), 從而不會(huì)被清除, 占用大量?jī)?nèi)存。
定期過(guò)期
將所有的 key 維護(hù)在一起, 每隔一段時(shí)間就從中掃描一定的數(shù)量的 key(采樣), 并清除其中已經(jīng)過(guò)期的 key。
通過(guò)調(diào)整定時(shí)掃描的時(shí)間間隔和每次掃描的耗時(shí), 可以在不同情況下使得 CPU 和內(nèi)存資源達(dá)到最優(yōu)的平衡效果。
在 Reids 的實(shí)現(xiàn)中是通過(guò) 惰性過(guò)期 + 定期過(guò)期 2 種策略配合, 達(dá)到內(nèi)存回收的效果。
2.2 惰性過(guò)期 在 Redis 中的實(shí)現(xiàn)
前提: Redis 中一個(gè)對(duì)象的過(guò)期時(shí)間存放在 dictEntry 的 v.s64 中, 至于 dictEntry 的設(shè)計(jì)可以看一下后面的附錄。
Redis 大部分讀寫(xiě)對(duì)象的命令, 在執(zhí)行前都會(huì)調(diào)用 expireIfNeeded 函數(shù)做一個(gè)過(guò)期檢查
- 如果 key 已經(jīng)過(guò)期了, 將其刪除
- 如果 key 未過(guò)期, 不做任何處理
expireIfNeeded 函數(shù)的定義如下
int expireIfNeeded(redisDb *db, robj *key) { // key 未過(guò)期返回 0 if (!keyIsExpired(db,key)) return 0; // 下面的邏輯都是 Key 過(guò)期的邏輯處理 // 當(dāng)前的節(jié)點(diǎn)是從節(jié)點(diǎn), 返回 1, 然后結(jié)束 // 為了保持主從數(shù)據(jù)的一致, 從節(jié)點(diǎn)不會(huì)主動(dòng)清除數(shù)據(jù), 都是主節(jié)點(diǎn)同步消息在刪除 if (server.masterhost != NULL) return 1; // 已經(jīng)刪除過(guò)期鍵個(gè)數(shù) + 1 server.stat_expiredkeys++; // 向從節(jié)點(diǎn)和 AOF 文件傳播 key 過(guò)期信息, 清除過(guò)期 key propagateExpire(db,key,server.lazyfree_lazy_expire); // 發(fā)送事件通知 notifyKeyspaceEvent(NOTIFY_EXPIRED,"expired",key,db->id); // lazyfree-lazy-expire 配置參數(shù) (版本 4.0 以上支持), 默認(rèn)為 0 // 根據(jù)配置, 同步或異步刪除 key (異步刪除: 先將 key 邏輯刪除, 然后在通過(guò)后臺(tái)的線程池進(jìn)行真正的空間釋放) return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) : dbSyncDelete(db,key); } int keyIsExpired(redisDb *db, robj *key) { // 從過(guò)期字典中獲取 key 對(duì)應(yīng)的過(guò)期時(shí)間, 實(shí)際就是獲取 dictEntity 的 v 中的 s64 值 (dictEntity.v.s64) mstime_t when = getExpire(db,key); mstime_t now; // 沒(méi)有過(guò)期時(shí)間 if (when < 0) return 0; // redis 在加載數(shù)據(jù)中 if (server.loading) return 0; // 獲取當(dāng)前的事件 if (server.lua_caller) { // 有 lua 腳本在執(zhí)行中, 當(dāng)前時(shí)間等于腳本開(kāi)始執(zhí)行前的時(shí)間 now = server.lua_time_start; } else if (server.fixed_time_expire > 0) { // 有緩存時(shí)間, 線使用緩存時(shí)間 // server.mstime 這個(gè)時(shí)間會(huì)在調(diào)用執(zhí)行命令函數(shù)的 call() 前進(jìn)行更新 // 這樣可以避免一些批量操作的命令, 比如 RPOPLPUSH 等命令, 這些命令會(huì)執(zhí)行過(guò)程中可能多次訪問(wèn)這個(gè) key // 而在多次的訪問(wèn)過(guò)程中, 可能出現(xiàn)上一次訪問(wèn)未過(guò)期, 下次訪問(wèn)已經(jīng)過(guò)期了, 通過(guò)這個(gè)緩沖時(shí)間可以解決這個(gè)問(wèn)題 now = server.mstime; } else { // 其他情況, 直接獲取當(dāng)前時(shí)間 now = mstime(); } // 當(dāng)前時(shí)間是否大于 key 的過(guò)期時(shí)間 return now > when; }
expireIfNeeded 的調(diào)用時(shí)機(jī), 基本都是在各個(gè)命令內(nèi)部。 以 String 的 get 命令為例, 大體的流程如下
/** * get 命令對(duì)應(yīng)的執(zhí)行函數(shù) * 需要的參數(shù)都封裝在 client 對(duì)象中 */ void getCommand(client *c) { // getGenericCommand -> lookupKeyReadOrReply -> lookupKeyRead -> lookupKeyReadWithFlags // getGenericCommand 經(jīng)過(guò)幾個(gè)函數(shù)最終調(diào)用到 lookupKeyReadWithFlags getGenericCommand(c); } robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) { robj *val; // expireIfNeeded 返回 > 0, 過(guò)期了 if (expireIfNeeded(db,key) == 1) { // 省略過(guò)期處理 // 過(guò)期的處理, 然后 return null } // 非過(guò)期處理, 查找然后返回 val = lookupKey(db,key,flags); if (val == NULL) server.stat_keyspace_misses++; else server.stat_keyspace_hits++; return val; }
上面就是 get 指令的中的惰性過(guò)期的過(guò)程, 其他命令的邏輯差不多, 核心就是一個(gè) expireIfNeeded 函數(shù)。
2.3 定期過(guò)期在 Redis 中的實(shí)現(xiàn)
Redis 默認(rèn)是 16 個(gè)數(shù)據(jù)庫(kù), 每個(gè)數(shù)據(jù)庫(kù)會(huì)將設(shè)置了過(guò)期時(shí)間的 key 放到各自的一個(gè)獨(dú)立的字典中, 稱(chēng)為過(guò)期字典 (redisDb 對(duì)象的 dict *expires 屬性)。
然后 Redis 默認(rèn)會(huì)按照每秒 10 次的頻率(可以通過(guò) redis.conf 中的 hz 配置)進(jìn)行過(guò)期掃描。
掃描的過(guò)程不會(huì)遍歷整個(gè)過(guò)期字典,而是按照以下策略進(jìn)行
- 從過(guò)期字典中隨機(jī)選擇 20 個(gè) key
- 刪除其中已經(jīng)過(guò)期的鍵
- 如果超過(guò) 25% 的鍵被刪除, 則重復(fù)步驟 1, 2, 3, 沒(méi)有超過(guò), 就結(jié)束這次掃描
- 同時(shí)為防止重復(fù)循環(huán), 導(dǎo)致線程卡死, 增加了每 16 次抽樣, 就做一次掃描時(shí)間的上限的檢查 (默認(rèn)是慢模式下, 上限是 25 毫秒, 如果是快模式,掃描上限是 1 毫秒), 超過(guò)就結(jié)束循環(huán)
定期過(guò)期刪除的實(shí)現(xiàn)主要在 /activeExpireCycle 函數(shù), 大體的邏輯如下
/** * 過(guò)期循環(huán)清除 * 為了便于理解, 這里對(duì)函數(shù)的邏輯做了一點(diǎn)小調(diào)整和刪除一些非必要的邏輯, 但是整體的邏輯不變 * @type 模式, 取值有 2 個(gè) ACTIVE_EXPIRE_CYCLE_SLOW (0, 慢模式), ACTIVE_EXPIRE_CYCLE_FAST (1, 快模式) */ void activeExpireCycle(int type) { // 靜態(tài)變量, 當(dāng)前處理的數(shù)據(jù)庫(kù)索引 // 靜態(tài)的效果, 這個(gè)變量執(zhí)行后的值不會(huì)被清空, 每次調(diào)用這個(gè)方法, 是上一次執(zhí)行的值 // 這樣就可以保證 16 個(gè)數(shù)據(jù)庫(kù), 每次方法執(zhí)行完, 下次進(jìn)來(lái)可以執(zhí)行到下一個(gè)數(shù)據(jù)庫(kù), 循環(huán)起來(lái),而不是每次進(jìn)來(lái)都從第 0 個(gè)開(kāi)始 static unsigned int current_db = 0; // 上一次清理是否是因?yàn)闀r(shí)間超時(shí)結(jié)束循環(huán)的, 同樣是靜態(tài)變量 static int timelimit_exit = 0; // 上一次快速循環(huán)循環(huán)的時(shí)間, 同樣是靜態(tài)變量 static long long last_fast_cycle = 0; // 當(dāng)前時(shí)間 long long start = ustime(), // 本次循環(huán)清除是快速循環(huán), 上一次是時(shí)間超時(shí)獲取 2 次快速循環(huán)的時(shí)間差在 2 毫秒內(nèi), 不執(zhí)行 if (type == ACTIVE_EXPIRE_CYCLE_FAST) { // 上一次循環(huán)是因?yàn)闀r(shí)間超時(shí)結(jié)束的, 本次快速循環(huán)不進(jìn)行 if (!timelimit_exit) return; // 上次快速循環(huán)距離當(dāng)前時(shí)間在 1000 * 2 = 2 毫秒內(nèi), 也不進(jìn)行快速循環(huán) if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return; last_fast_cycle = start; } // 計(jì)算循環(huán)的上限毫秒限制 // server.hz 默認(rèn)等于 10, ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 等于 25 // 1000000 * 25 / 10 / 100 = 25000 單位: 微秒, 即 25 毫秒 long long timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100; // ACTIVE_EXPIRE_CYCLE_FAST_DURATION = 1000 // 如果是快模式, 修改為 1000 微秒, 即 1 毫秒超時(shí) if (type == ACTIVE_EXPIRE_CYCLE_FAST) timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; // CRON_DBS_PER_CALL = 16, 每次循環(huán)處理的數(shù)據(jù)庫(kù)數(shù)量 int dbs_per_call = CRON_DBS_PER_CALL; // 遍歷當(dāng)前數(shù)據(jù)庫(kù)的次數(shù) int iteration = 0; // 遍歷循環(huán) 16 個(gè)數(shù)據(jù)庫(kù) for (int j = 0; j < dbs_per_call && timelimit_exit == 0; j++) { // 清理過(guò)期的 key 個(gè)數(shù) int expired; // 計(jì)算本次處理的數(shù)據(jù)庫(kù) redisDb *db = server.db+(current_db % server.dbnum); current_db++; do { // 開(kāi)始循環(huán)清除當(dāng)前數(shù)據(jù)庫(kù)中過(guò)期的 key // 遍歷次數(shù) + 1 iteration++; // dictSize 獲取整個(gè)過(guò)期字典的已經(jīng)使用大小 unsigned long num = dictSize(db->expires); // num == 0 表示整個(gè)字典沒(méi)有數(shù)據(jù), 跳出循環(huán),處理下一個(gè)數(shù)據(jù)庫(kù) if (num == 0) { break; } // 計(jì)算整個(gè)過(guò)期字典的總大小 unsigned long slots = dictSlots(db->expires); // DICT_HT_INITIAL_SIZE = 4, 每個(gè)字典初始化時(shí)的默認(rèn)值 // num > 0, 字典中有數(shù)據(jù)了, slots 大于 4, 表示當(dāng)前的字典擴(kuò)容過(guò)了 // num && slots > DICT_HT_INITIAL_SIZE, 當(dāng)前的字典擴(kuò)容過(guò)同時(shí)里面有數(shù)據(jù) // num * 100 / slots < 1 計(jì)算當(dāng)前使用的數(shù)據(jù)占整個(gè)字典的百分比是否小于 1% // Redis 認(rèn)為, 如果一個(gè)字典中的使用率小于 1%, 花時(shí)間去進(jìn)行清理是一個(gè)昂貴的操作 // 應(yīng)該停下來(lái),等待更好的時(shí)間再進(jìn)行調(diào)整 // 所以簡(jiǎn)單理解: 當(dāng)這個(gè)字典中使用的空間小于 1%, 這里跳過(guò)了這個(gè)數(shù)據(jù)的處理 if (num && slots > DICT_HT_INITIAL_SIZE && (num * 100 / slots < 1)) break; expired = 0; // ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP = 20 // 本次從過(guò)期字典中獲取多少個(gè) key, 如果字典中的已經(jīng)使用的 key 大于 20, 則只取 20 個(gè), 否則有多少取多少 if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP) num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP; // 循環(huán) num 次從字典中獲取 key while (num--) { dictEntry *de; // 從過(guò)期字典中隨機(jī)獲取一個(gè) key, 獲取不到, 就停止本次循環(huán) if ((de = dictGetRandomKey(db->expires)) == NULL) break; // 嘗試釋放這個(gè) key, 如果 key 釋放成功, 過(guò)期次數(shù) + 1 if (activeExpireCycleTryExpire(db,de,now)) expired++; } // 0xf = 15, iteration 表示遍歷了 15 次 if ((iteration & 0xf) == 0) { // 計(jì)算消耗時(shí)間 int elapsed = ustime()-start; // 消耗時(shí)間超過(guò)了限制時(shí)間, 結(jié)束本次循環(huán) if (elapsed > timelimit) { // 超過(guò)時(shí)間限制標(biāo)識(shí)設(shè)置為 true, 本次循環(huán)清除超時(shí)了, 結(jié)束本次循環(huán)清除 timelimit_exit = 1; break; } } // 本次清理的過(guò)期 key 超過(guò)了 25%, 繼續(xù), 否則結(jié)束 // ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP = 20 // 每次抽取的個(gè)數(shù)最大為 20 個(gè), 控制 25%, 20 * 25% = 5 個(gè) // 也就是過(guò)期的個(gè)數(shù)大于 5 就是大于 25%, (ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4 = 5) } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4); } // 省略各種分析數(shù)據(jù)的記錄 }
調(diào)用 activeExpireCycle 的入口有 2 個(gè)
- Redis 定時(shí)事件觸發(fā)
/** * Reids 啟動(dòng)時(shí), 向事件輪詢中注冊(cè)的唯一一個(gè)定時(shí)事件(默認(rèn) 100 毫秒執(zhí)行一次), 執(zhí)行的函數(shù) */ int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { ... // 數(shù)據(jù)庫(kù)掃描 databasesCron(); ... } void databasesCron(void) { // 過(guò)期功能開(kāi)啟中, 默認(rèn)為開(kāi)啟 if (server.active_expire_enabled) { // 主節(jié)點(diǎn) if (server.masterhost == NULL) { // 慢模式循環(huán)清除 activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW); } else { // 從節(jié)點(diǎn)處理 expireSlaveKeys(); } } ... }
- 事件輪詢中, 進(jìn)入阻塞前的調(diào)用函數(shù)
void beforeSleep(struct aeEventLoop *eventLoop) { ... // 過(guò)期功能開(kāi)啟中同時(shí)為主節(jié)點(diǎn) if (server.active_expire_enabled && server.masterhost == NULL) // 快模式循環(huán)清除 activeExpireCycle(ACTIVE_EXPIRE_CYCLE_FAST); ... }
3 內(nèi)存淘汰
3.1 淘汰算法
為了能夠騰出內(nèi)存空間, 需要在一大群對(duì)象中選擇某一些進(jìn)行淘汰, 哪么應(yīng)該基于什么標(biāo)準(zhǔn)進(jìn)行選擇呢?
比較常見(jiàn)的算法有 2 個(gè): LRU 和 LFU。
LRU (Least Recently Used): 最近最少使用算法, 根據(jù)數(shù)據(jù)的歷史訪問(wèn)記錄進(jìn)行淘汰數(shù)據(jù),優(yōu)先移除最近最少使用的數(shù)據(jù)。
簡(jiǎn)單理解就是根據(jù)對(duì)象的訪問(wèn)時(shí)間, 優(yōu)先淘汰訪問(wèn)時(shí)間最早的對(duì)象。
LFU (Least Frequently Used): 最少頻率使用算法, 根據(jù)數(shù)據(jù)的訪問(wèn)頻率頻率進(jìn)行淘汰數(shù)據(jù), 優(yōu)先移除最近使用頻率最少的數(shù)據(jù)。
簡(jiǎn)單理解就是根據(jù)對(duì)象的訪問(wèn)次數(shù), 優(yōu)先淘汰訪問(wèn)次數(shù)最少的對(duì)象。
3.2 Redis 內(nèi)存淘汰策略
在 LFU 和 LRU 的基礎(chǔ)上, Redis 提供了 8 種淘汰策略
策略 | 說(shuō)明 |
---|---|
noeviction | 默認(rèn)策略, 不會(huì)刪除任何數(shù)據(jù), 但是拒絕所有寫(xiě)入操作并返回客戶端錯(cuò)誤信息 (error)OOM command not allow when used memory。此時(shí) Redis 只響應(yīng)讀操作。 |
volatile-lru | Least Recently Used, 最近最少使用。在所有設(shè)置了 expire 的 key 中刪除最近最少使用的鍵值對(duì), 即距離上次訪問(wèn)時(shí)間最久的。 |
allkeys-lru | Least Recently Used, 最近最少使用。在所有的 key 中刪除最近最少使用的鍵值對(duì), 即距離上次訪問(wèn)時(shí)間最久的。 |
volatile-lfu | Least Frequently Used, 最不經(jīng)常使用。在所有設(shè)置了 expire 的 key 中刪除最不經(jīng)常使用的鍵值對(duì), 即訪問(wèn)次數(shù)最少的。 |
allkeys-lfu | Least Frequently Used, 最不經(jīng)常使用。在所有的 key 中刪除最不經(jīng)常使用的鍵值對(duì), 即訪問(wèn)次數(shù)最少的。 |
volatile-random | 在所有設(shè)置了 expire 的 key 中隨機(jī)選擇刪除 |
allkeys-random | 在所有的 key 中隨機(jī)選擇刪除。 |
volatile-ttl | Time To Live, 存活時(shí)間。 在所有設(shè)置了 expire 的 key 中刪除 ttl 值最多的。 |
volatile-lru, volatile-random, volatile-ttl, 在沒(méi)有符合條件的 key 的情況下, 會(huì)按照 noeviction 的策略進(jìn)行處理。
3.3 Redis 對(duì)象淘汰判斷標(biāo)準(zhǔn)設(shè)計(jì)
在上面介紹的幾種策略可以知道, 要判斷一個(gè)對(duì)象是否可以被淘汰, 需要對(duì)象自身存放使用策略對(duì)應(yīng)的數(shù)據(jù), 以便于判斷
比如:
2 個(gè) lru 策略, 需要對(duì)象自身保存好上次訪問(wèn)的時(shí)間
2 個(gè) lfu 策略, 需要對(duì)象自身保存好訪問(wèn)次數(shù)
ttl 策略, 需要對(duì)象自身保存好過(guò)期時(shí)間
2 個(gè) random 策略, 不需要保存額外的數(shù)據(jù), 通過(guò)隨機(jī)一個(gè)數(shù), 根據(jù)這個(gè)數(shù)從字典中獲取數(shù)據(jù)即可
3.3.1 Redis 對(duì)象的設(shè)計(jì)
正常情況下, 當(dāng)我們向 Redis 中存入一對(duì)鍵值對(duì), 實(shí)際可以拆分為 2 個(gè)對(duì)象, 一個(gè) key, 一個(gè) value。
其中 key 可以明確為是一個(gè)字符串, 所以存入到 Redis 的鍵值對(duì)的 key 會(huì)被封裝為 sds 對(duì)象。
但是 value 可以類(lèi)型可以很多, 為了行為的統(tǒng)一等, 需要對(duì) value 做一個(gè)封裝, 落實(shí)到源碼中就是一個(gè) redisObject 對(duì)象, 其定義如下
typedef struct redisObject { /** * 標(biāo)識(shí)這個(gè)對(duì)象的數(shù)據(jù)類(lèi)型, 常說(shuō)的 String, Hash, List 等 */ unsigned type:4; /** * 可以理解為數(shù)據(jù)類(lèi)型的具體實(shí)現(xiàn)類(lèi)型 * 比如數(shù)據(jù)類(lèi)型為 List, 在具體的實(shí)現(xiàn)中可以是 ArrayList LinkedList 等 */ unsigned encoding:4; /** * LRU_BITS = 24, * 一個(gè) 24 位的變量, 表示對(duì)象最后一次被程序訪問(wèn)的時(shí)間或者訪問(wèn)的次數(shù), 與內(nèi)存回收有關(guān) * 暫時(shí)知道有這個(gè)對(duì)象即可, 后面有分析 */ unsigned lru:LRU_BITS; /** * 被引用的次數(shù), 當(dāng) refcount 為 0 的時(shí)候, 表示該對(duì)象已經(jīng)不被任何對(duì)象引用, 則可以進(jìn)行垃圾回收了 */ int refcount; /** * 一個(gè)指針, 指向具體的數(shù)據(jù) */ void *ptr; } robj;
一個(gè)對(duì)象的 lru 和 lfu 計(jì)算后的值, 都是存放在這個(gè)對(duì)象的 lru 字段中的, 但是 lru 和 lfu 的計(jì)算方式是不一樣的。
3.3.2 lru 策略, 對(duì)象的訪問(wèn)時(shí)間設(shè)計(jì)
3.3.2.1 全局時(shí)間 lruclock
在 Redis 的中維護(hù)了一個(gè)全局的變量 lruclock, 表示當(dāng)前時(shí)間的一個(gè)相對(duì)值。
/** * redisServer 可以看做整個(gè) Redis 運(yùn)行時(shí)的上下文, 保存的數(shù)據(jù), 配置等都在這個(gè)結(jié)構(gòu)體中 */ struct redisServer { unsigned int lruclock = getLRUClock(); } unsigned int getLRUClock(void) { // LRU_CLOCK_RESOLUTION = 1000 // mstime() 當(dāng)前時(shí)間毫秒, 當(dāng)前時(shí)間的毫秒/LRU_CLOCK_RESOLUTION = 當(dāng)前時(shí)間的毫秒/1000 = 變?yōu)閱挝幻? // LRU_CLOCK_MAX = ((1<<LRU_BITS)-1) = 1<<24-1 = redisObject lru 字段的最大值 // (當(dāng)前的時(shí)間 / 1000) & (1<<24-1) 確保時(shí)間的精度是秒, 同時(shí)不會(huì)超過(guò) 24 位的整數(shù)的最多值 // 整個(gè)全局時(shí)間的進(jìn)度為秒, 2 個(gè)對(duì)象的訪問(wèn)時(shí)間差如果在秒內(nèi), 得到的是他們的訪問(wèn)時(shí)間是一樣的 // 得到一個(gè)當(dāng)前時(shí)間的相對(duì)值 return (mstime()/LRU_CLOCK_RESOLUTION) & LRU_CLOCK_MAX; }
同時(shí)這個(gè)時(shí)間會(huì)在 Redis 的定時(shí)任務(wù) serverCron 中定時(shí)的更新為最新的值
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { // serverCron 默認(rèn)是 100 毫秒執(zhí)行一次 unsigned int lruclock = getLRUClock(); atomicSet(server.lruclock,lruclock); }
3.3.2.2 對(duì)象的訪問(wèn)時(shí)間設(shè)計(jì)
Redis 每次通過(guò) key 在數(shù)據(jù)庫(kù)中查詢對(duì)應(yīng)的 value 時(shí), 在找到時(shí), 就會(huì)進(jìn)行 lru 字段的更新
robj *lookupKey(redisDb *db, robj *key, int flags) { // 從字典中獲取 key 對(duì)應(yīng)的 dictEntry (字典的設(shè)計(jì)可以看一下后面的附錄) dictEntry *de = dictFind(db->dict,key->ptr); if (de) { // 獲取 key 對(duì)應(yīng)的 dictEntry 的存在 // 獲取 dictEntry 的 value 也就是 redisObject 對(duì)象 robj *val = dictGetVal(de); if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 && !(flags & LOOKUP_NOTOUCH)) { // 沒(méi)有在進(jìn)行 RDB 或 AOF 操作, 并且 flags 沒(méi)有設(shè)置 LOOKUP_NOTOUCH // 淘汰策略設(shè)置的的 LFU 策略 if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) { updateLFU(val); } else { // 其他策略, 更新 lru 為全局的 lruclock val->lru = LRU_CLOCK(); } } } else { // key 不存在, 返回 null return NULL; } } unsigned int LRU_CLOCK(void) { unsigned int lruclock; // LRU_CLOCK_RESOLUTION = 1000 // 1000/server.hz 就是上面定時(shí)任務(wù) serverCron 的執(zhí)行時(shí)間 // <= 1000 說(shuō)明 serverCron 的執(zhí)行時(shí)間小于 1 秒, 直接獲取 server.lruclock 的值 // 如果大于 1000, 就調(diào)用 getLRUClock() 實(shí)時(shí)獲取當(dāng)前的時(shí)間, 因?yàn)轭l率太低了, 會(huì)造成更多的對(duì)象的訪問(wèn)時(shí)間一樣 if (1000/server.hz <= LRU_CLOCK_RESOLUTION) { atomicGet(server.lruclock,lruclock); } else { lruclock = getLRUClock(); } return lruclock; }
3.3.3 lfu 策略, 對(duì)象的訪問(wèn)頻率設(shè)計(jì)
對(duì)象的 lfu 同樣是存放在 redisObject 的 lru:LRU_BITS 字段。 這個(gè) 24 bits 字段, 被分為兩部分
高 16 位用來(lái)記錄訪問(wèn)時(shí)間 (單位為分鐘,ldt, last decrement time)
低 8 位用來(lái)記錄相對(duì)的訪問(wèn)次數(shù), 簡(jiǎn)稱(chēng) counter (logc, logistic counter)
Redis 中對(duì) LFU 的實(shí)現(xiàn)比較特殊, 通過(guò)時(shí)間衰減的方式近似達(dá)到了 LFU 的效果。
大體的思路如下:
對(duì)象創(chuàng)建時(shí), 初始訪問(wèn)次數(shù)為 5 (避免剛創(chuàng)建出來(lái), 對(duì)象就被回收), 同時(shí)記錄下當(dāng)前時(shí)間, 單位分鐘
對(duì)象被訪問(wèn)時(shí), 獲取當(dāng)前時(shí)間, 單位分鐘, 當(dāng)前時(shí)間 - 對(duì)象本身記錄的時(shí)間, 得到相差多少分鐘, 訪問(wèn)次數(shù)就減少多少
然后對(duì)象的訪問(wèn)次數(shù) + 1, 再次記錄下當(dāng)前時(shí)間
這樣對(duì)象在單位分鐘內(nèi), 訪問(wèn)越頻繁, 訪問(wèn)次數(shù)越大, 同時(shí)隨著時(shí)間的推移, 沒(méi)有進(jìn)行訪問(wèn), 訪問(wèn)次數(shù)會(huì)逐漸減少, 從而達(dá)到了 LFU 的效果。
ldt 記錄的是最近一次訪問(wèn)的時(shí)間, 16 位, 所以最大值為 65535, 單位是分鐘, 差不多 45 天左右。
也就是一個(gè)對(duì)象如果一直被訪問(wèn), 到了第 45 天后, 這個(gè)值又會(huì)重新回到 0 開(kāi)始計(jì)算。
ldt 的計(jì)算
unsigned long LFUGetTimeInMinutes(void) { // & 65535 保證時(shí)間的范圍在 0 ~ 65535 之間, 不會(huì)超過(guò) 16 數(shù)值的大小 return (server.unixtime/60) & 65535; }
同 lru 一樣, lruclock 的計(jì)算, 后面的時(shí)間比前面的時(shí)間小,
說(shuō)明后面的時(shí)間到了下一輪的重新開(kāi)始了, 這時(shí)只需要后面的時(shí)間 + 65535 - 前面的時(shí)間, 就能得到 2 個(gè)時(shí)間的差值了。
logc 記錄的是一個(gè)相對(duì)的訪問(wèn)次數(shù)。
本身只有 8 位, 也就是最大值為 255, 也就是一個(gè)對(duì)象只能保存 255 次訪問(wèn)次數(shù), 這個(gè)基本不同滿足日常的使用。
所以 Redis 內(nèi)部設(shè)計(jì)了一個(gè)隨機(jī)公式, 控制訪問(wèn)次數(shù)的增長(zhǎng), 即每次訪問(wèn), 訪問(wèn)次數(shù)加不加一, 通過(guò)隨機(jī)判斷。
uint8_t LFULogIncr(uint8_t counter) { // 當(dāng)前的訪問(wèn)次數(shù)已經(jīng)達(dá)到了最大值了 if (counter == 255) return 255; // 產(chǎn)生一個(gè)隨機(jī)數(shù) double r = (double)rand()/RAND_MAX; // 獲取一個(gè)基礎(chǔ)值, 當(dāng)前的次數(shù) - 對(duì)象初始化的默認(rèn)次數(shù) (LFU_INIT_VAL = 5) double baseval = counter - LFU_INIT_VAL; if (baseval < 0) baseval = 0; // 1.0 / 基礎(chǔ)值 * server.lfu_log_factor (默認(rèn)值, 10, 可配置) + 1, 得到一個(gè)數(shù) double p = 1.0/(baseval*server.lfu_log_factor+1); // 得到的數(shù)大于隨機(jī)出來(lái)的數(shù), 訪問(wèn)次數(shù) + 1 if (r < p) counter++; return counter; }
官方的測(cè)試數(shù)據(jù) (可以簡(jiǎn)單看成, counter = 5, 在 100 - 1000w 次的調(diào)用, lfu_log_factor 不同取值下, 最終的 counter 的值)
lfu_log_factor 取值 | 100 次 | 1000 次 | 10w 次 | 100w 次 | 1000w 次 |
---|---|---|---|---|---|
0 | 104 | 255 | 255 | 255 | 255 |
1 | 18 | 49 | 255 | 255 | 255 |
10 | 10 | 18 | 142 | 255 | 255 |
100 | 8 | 11 | 49 | 143 | 255 |
lfu_log_factor 設(shè)置為 10 的情況下, 在 100w 次的訪問(wèn)中, 訪問(wèn)次數(shù)才達(dá)到為 255, 也就是最大值。
基本可以滿足 10w 次的使用
3.3.3.1 counter 衰減機(jī)制
每個(gè)對(duì)象被返回時(shí), counter 都會(huì)先進(jìn)行一個(gè)衰減操作, 然后再通過(guò)上面的隨機(jī)公式進(jìn)行判斷次數(shù)是否需要增加。
衰減的過(guò)程如下
unsigned long LFUDecrAndReturn(robj *o) { // 右移 8 為, 也就是得的了高位的 16 位, 即 ldt, 得到上次記錄的時(shí)間 unsigned long ldt = o->lru >> 8; // 得到當(dāng)前保存的次數(shù) unsigned long counter = o->lru & 255; // lfu_decay_time 衰減時(shí)間, 默認(rèn) 1, 單位分鐘 // 如果沒(méi)有配置 lfu_decay_time, 則默認(rèn)不進(jìn)行衰減, counter 當(dāng)前是多少就是多少 // 獲取 2 次訪問(wèn)的時(shí)間差 / lfu_decay_time, 得到經(jīng)過(guò)了多少個(gè)時(shí)間段 unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : 0; if (num_periods) // 最新的次數(shù) = 當(dāng)前的次數(shù) - 經(jīng)過(guò)了多少個(gè)時(shí)間段, 小于 0 時(shí), 設(shè)置為 0 counter = (num_periods > counter) ? 0 : counter - num_periods; return counter; } // 距離上次訪問(wèn)相差多少分鐘 unsigned long LFUTimeElapsed(unsigned long ldt) { unsigned long now = LFUGetTimeInMinutes(); if (now >= ldt) return now-ldt; return 65535-ldt+now; }
3.3.3.2 對(duì)象的訪問(wèn)頻率設(shè)計(jì)
Redis 每次通過(guò) key 在數(shù)據(jù)庫(kù)中查詢對(duì)應(yīng)的 value 時(shí), 在找到時(shí), 就會(huì)進(jìn)行 lru 字段的更新
robj *lookupKey(redisDb *db, robj *key, int flags) { dictEntry *de = dictFind(db->dict,key->ptr); if (de) { robj *val = dictGetVal(de); if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 && !(flags & LOOKUP_NOTOUCH)) { // 淘汰策略設(shè)置的的 LFU 策略 if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) { updateLFU(val); } else { val->lru = LRU_CLOCK(); } } } else { return NULL; } } void updateLFU(robj *val) { // 通過(guò)衰減機(jī)制, 得到最新的 counter unsigned long counter = LFUDecrAndReturn(val); // 通過(guò)隨機(jī)公式, 得到最新的 counter counter = LFULogIncr(counter); // 將最新的 counter 和 當(dāng)前時(shí)間保存到 lru 字段中 val->lru = (LFUGetTimeInMinutes()<<8) | counter; }
3.4 Redis 內(nèi)存淘汰策略的實(shí)現(xiàn)
Redis 的內(nèi)存的實(shí)現(xiàn)方式都是通過(guò)隨機(jī)采樣 + 比較 lru 值決定是否淘汰的方式實(shí)現(xiàn)的。
大體過(guò)程如下:
- Redis 啟動(dòng)時(shí), 會(huì)初始一個(gè)默認(rèn)容量為 16 的待淘汰數(shù)據(jù)池 evictionPoolEntry (本質(zhì)就是一個(gè)數(shù)組)
- 每個(gè)存入到 Redis 的對(duì)象 (redisObject) 都會(huì)在初始其 24 位的 lru 字段 (lru: 一個(gè)相對(duì)的訪問(wèn)時(shí)間, lfu: 一個(gè)相對(duì)的訪問(wèn)次數(shù))
- 后面每次訪問(wèn) Redis 的對(duì)象時(shí), 更新其 lru 字段的值
- 同時(shí)每次執(zhí)行一個(gè) Redis 命令時(shí), 就會(huì)判斷一下當(dāng)前的內(nèi)存是否足夠, 如果不夠, 就計(jì)算出需要釋放多少內(nèi)存, 然后進(jìn)行內(nèi)存淘汰
內(nèi)存淘汰的過(guò)程如下:
4.1 首次淘汰從數(shù)據(jù)字典或過(guò)期字典 (由配置的淘汰策略決定) 中隨機(jī)抽樣選出最多 N 個(gè)數(shù)據(jù)放入到一個(gè)樣例池。
數(shù)據(jù)量 N: 由 redis.conf 配置的 maxmemory-samples 決定, 默認(rèn)值是 5。 配置為 10 將非常接近真實(shí) LRU 效果。
采樣參數(shù) maxmemory-samples 配置的數(shù)值越大, 就越能精確的查找到待淘汰的緩存數(shù)據(jù), 但是也消耗更多的 CPU 計(jì)算, 執(zhí)行效率降低。
同時(shí)為了避免長(zhǎng)時(shí)間找不到足夠的數(shù)據(jù)填充樣例池, 強(qiáng)制寫(xiě)死了單次尋找數(shù)據(jù)的最大次數(shù)是 maxsteps = N*10。
4.2 再次淘汰遍歷整個(gè)樣例池, 遍歷的對(duì)象通過(guò) lru 計(jì)算處理的值, 只要比待淘汰數(shù)據(jù)池中的任意一條數(shù)據(jù)的小, 就將該數(shù)據(jù)填充至待淘汰數(shù)據(jù)池。
第一次淘汰時(shí), 待淘汰數(shù)據(jù)池為空, 所以第一次淘汰時(shí), 會(huì)將所有的樣例數(shù)據(jù)填充到待淘汰數(shù)據(jù)池中, 這個(gè)池子后面就都會(huì)有數(shù)據(jù), 一直存在著。
后續(xù)的淘汰時(shí), 樣例池 中的數(shù)據(jù)就有可能進(jìn)入到待淘汰數(shù)據(jù)池中, 也有可能不進(jìn)入。
4.3 執(zhí)行淘汰從待淘汰數(shù)據(jù)池的尾部向前找到第一個(gè)可以刪除的 key (此時(shí)找到的 key 就是值最小/大的, 既空閑時(shí)間最大/訪問(wèn)次數(shù)最小/存活時(shí)間最小), 對(duì)其進(jìn)行淘汰
4.4 繼續(xù)淘汰計(jì)算刪除了一個(gè) key 后內(nèi)存釋放了多少, 如果沒(méi)達(dá)到要求的釋放量, 就回到步驟 4.1 繼續(xù)淘汰
3.4.1 Redis 內(nèi)存淘汰策略的代碼實(shí)現(xiàn)
入口: 每個(gè)命令的執(zhí)行處
int processCommand(client *c) { ... // 有設(shè)置最大內(nèi)存 同時(shí)當(dāng)前沒(méi)有 lua 腳本超時(shí)的情況 if (server.maxmemory && !server.lua_timedout) { // 有必要時(shí), 嘗試釋放內(nèi)存 int out_of_memory = freeMemoryIfNeededAndSafe() == C_ERR; // 內(nèi)存不夠 同時(shí)執(zhí)行的命令是變更命令 或者 當(dāng)前的客戶端開(kāi)啟了事務(wù), 同時(shí)執(zhí)行的命令不是 exec if (out_of_memory && (c->cmd->flags & CMD_DENYOOM || (c->flags & CLIENT_MULTI && c->cmd->proc != execCommand))) { flagTransaction(c); // 響應(yīng) -OOM command not allowed when used memory > 'maxmemory' addReply(c, shared.oomerr); return C_OK; } } ... } int freeMemoryIfNeededAndSafe(void) { // 當(dāng)前有 lua 腳本執(zhí)行超時(shí)或者真正加載數(shù)據(jù), 返回成功 if (server.lua_timedout || server.loading) return C_OK; // 是否內(nèi)存如果有必要的話 return freeMemoryIfNeeded(); }
釋放內(nèi)存的核心函數(shù)
int freeMemoryIfNeeded(void) { // 如果是從節(jié)點(diǎn)同時(shí)配置了從節(jié)點(diǎn)忽略內(nèi)存配置, 直接返回 if (server.masterhost && server.repl_slave_ignore_maxmemory) return C_OK; // mem_reported 保存了整個(gè) Redis 已經(jīng)使用的內(nèi)存 // mem_tofree 經(jīng)過(guò)計(jì)算本次應(yīng)該釋放的內(nèi)存, 等于當(dāng)前已經(jīng)使用的內(nèi)存 - 用于主從復(fù)制的復(fù)制緩沖區(qū)大小 - 配置的 maxmemory // mem_freed 已經(jīng)釋放了多少內(nèi)存 size_t mem_reported, mem_tofree, mem_freed; long long delta; // 從節(jié)點(diǎn)個(gè)數(shù) int slaves = listLength(server.slaves); // 判斷當(dāng)前的內(nèi)存狀態(tài), 如果足夠, 直接返回 if (getMaxmemoryState(&mem_reported,NULL,&mem_tofree,NULL) == C_OK) return C_OK; // 如果配置的策略為 noeviction if (server.maxmemory_policy == MAXMEMORY_NO_EVICTION) goto cant_free; mem_freed = 0; // 沒(méi)有達(dá)到需要的內(nèi)存大小, 繼續(xù)循環(huán) while (mem_freed < mem_tofree) { static unsigned int next_db = 0; sds bestkey = NULL; int bestdbid; redisDb *db; dict *dict; dictEntry *de; if (server.maxmemory_policy & (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU) || server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL) { // LRU + LFU + TTL 策略 // 淘汰池 struct evictionPoolEntry *pool = EvictionPoolLRU; while(bestkey == NULL) { // 遍歷 16 個(gè)數(shù)據(jù)庫(kù) for (i = 0; i < server.dbnum; i++) { db = server.db+i; // 根據(jù) volatile 或 all 選擇對(duì)應(yīng)的數(shù)據(jù)字典 dict = (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) ? db->dict : db->expires; // 獲取字典的數(shù)據(jù)大小, keys 為當(dāng)前數(shù)據(jù)庫(kù)的 key 的數(shù)量 if ((keys = dictSize(dict)) != 0) { evictionPoolPopulate(i, dict, db->dict, pool); total_keys += keys; } } // 沒(méi)有可以處理的 keys if (!total_keys) break; // EVPOOL_SIZE = 16 for (k = EVPOOL_SIZE-1; k >= 0; k--) { if (pool[k].key == NULL) continue; bestdbid = pool[k].dbid; // 從數(shù)據(jù)庫(kù)中獲取對(duì)應(yīng)的節(jié)點(diǎn) if (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) { de = dictFind(server.db[pool[k].dbid].dict, pool[k].key); } else { de = dictFind(server.db[pool[k].dbid].expires, pool[k].key); } // 釋放緩存 if (pool[k].key != pool[k].cached) sdsfree(pool[k].key); pool[k].key = NULL; pool[k].idle = 0; // 找到的釋放對(duì)象存在, 先跳出這次循環(huán) if (de) { bestkey = dictGetKey(de); break; } else { // 不存在, 進(jìn)行循環(huán)查找 } } } } else if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM || server.maxmemory_policy == MAXMEMORY_VOLATILE_RANDOM) { // random 策略 } // 刪除找到的 key if (bestkey) { db = server.db+bestdbid; // 將 key 封裝為 redisObject 對(duì)象 robj *keyobj = createStringObject(bestkey,sdslen(bestkey)); // 傳播 key 過(guò)期信息到主從復(fù)制和 AOF 文件 propagateExpire(db,keyobj,server.lazyfree_lazy_eviction); // 獲取當(dāng)前的內(nèi)存大小 delta = (long long) zmalloc_used_memory(); // 同步刪除或異步刪除 key if (server.lazyfree_lazy_eviction) { dbAsyncDelete(db,keyobj); else dbSyncDelete(db,keyobj); } // 計(jì)算本次釋放的內(nèi)存 delta -= (long long) zmalloc_used_memory(); mem_freed += delta; // 釋放創(chuàng)建的 key redisObject 對(duì)象 decrRefCount(keyobj); keys_freed++; // 如果有從節(jié)點(diǎn), 推送緩沖區(qū)的數(shù)據(jù) if (slaves) flushSlavesOutputBuffers(); // 支持異步清除 同時(shí) 清除了 16 個(gè) key if (server.lazyfree_lazy_eviction && !(keys_freed % 16)) { // 再次判斷內(nèi)存情況, 如果內(nèi)存足夠了 if (getMaxmemoryState(NULL,NULL,NULL,NULL) == C_OK) { // 更新已經(jīng)釋放的緩存大小 = 需要釋放的緩存大小 mem_freed = mem_tofree; } } } // 本次釋放沒(méi)有處理成功任何一個(gè) key if (!keys_freed) { goto cant_free; } } return C_OK; cant_free: // 沒(méi)有內(nèi)存可以分配了, 做唯一可以做的一件事: 檢查是否有 lazyfree 線程在執(zhí)行釋放內(nèi)存任務(wù), 有進(jìn)行等待 // 知道沒(méi)有任務(wù)或者已有的內(nèi)存達(dá)到了需要釋放的內(nèi)存 while(bioPendingJobsOfType(BIO_LAZY_FREE)) { // 當(dāng)前的內(nèi)存達(dá)到了現(xiàn)在需要的釋放的內(nèi)存, 結(jié)束檢查 if (((mem_reported - zmalloc_used_memory()) + mem_freed) >= mem_tofree) break; usleep(1000); } return C_ERR;
淘汰池的填充
void evictionPoolPopulate(int dbid, dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) { int j, k, count; // 采樣結(jié)果數(shù)組, 最大容量為 mamemory_samples 的大小 dictEntry *samples[server.maxmemory_samples]; // 從 sampledict 字典中采樣 server.maxmemory_samples 個(gè) key 存放到 samples, 同時(shí)返回總共采樣的多少個(gè) count = dictGetSomeKeys(sampledict,samples,server.maxmemory_samples); for (j = 0; j < count; j++) { unsigned long long idle; sds key; robj *o; dictEntry *de; de = samples[j]; key = dictGetKey(de); if (server.maxmemory_policy != MAXMEMORY_VOLATILE_TTL) { if (sampledict != keydict) de = dictFind(keydict, key); o = dictGetVal(de); } if (server.maxmemory_policy & MAXMEMORY_FLAG_LRU) { // LRU 算法 idle = estimateObjectIdleTime(o); } else if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) { // LRU 算法 idle = 255 - LFUDecrAndReturn(o); } else if (server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL) { // TTL 算法 idle = ULLONG_MAX - (long)dictGetVal(de); } else { serverPanic("Unknown eviction policy in evictionPoolPopulate()"); } k = 0; // 從 evictionPoolEntry 淘汰池中找到第一個(gè)閑置時(shí)間比當(dāng)前淘汰 key 大的 while (k < EVPOOL_SIZE && pool[k].key && pool[k].idle < idle) k++; if (k == 0 && pool[EVPOOL_SIZE-1].key != NULL) { // 如果找到的 key 比淘汰池中閑置時(shí)間最小的 key 還小, 同時(shí)淘汰池沒(méi)有空間了, 則跳過(guò)這個(gè) key continue; } else if (k < EVPOOL_SIZE && pool[k].key == NULL) { // 插入的位置為空, 直接進(jìn)入到下面的賦值節(jié)點(diǎn) } else { // 核心就是將找到的位置 k 空出來(lái) // 最后的位置為空 if (pool[EVPOOL_SIZE-1].key == NULL) { // 將原本 k 位置和后面的數(shù)據(jù)向后移動(dòng) 1 位 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 { // 插入的位置不為空 // 將原本 k 位置前面的數(shù)據(jù)往前移動(dòng) 1 位, 原本的第一位丟棄 k--; sds cached = pool[0].cached; if (pool[0].key != pool[0].cached) sdsfree(pool[0].key); memmove(pool,pool+1,sizeof(pool[0])*k); pool[k].cached = cached; } } // 把找到的 key 放到 k 的位置 int klen = sdslen(key); // EVPOOL_CACHED_SDS_SIZE = 255 if (klen > EVPOOL_CACHED_SDS_SIZE) { // 創(chuàng)建一個(gè)新的 key 賦值給 pool[k].key pool[k].key = sdsdup(key); } else { // 從 key 中拷貝 klen + 1 的長(zhǎng)度到 pool[k].cached memcpy(pool[k].cached,key,klen+1); sdssetlen(pool[k].cached,klen); pool[k].key = pool[k].cached; } pool[k].idle = idle; pool[k].dbid = dbid; } } unsigned int dictGetSomeKeys(dict *d, dictEntry **des, unsigned int count) { unsigned long j; unsigned long tables; unsigned long stored = 0, maxsizemask; unsigned long maxsteps; // 字典中的數(shù)據(jù)量小于需要的個(gè)數(shù), 取的個(gè)數(shù)變?yōu)樽值涞臄?shù)據(jù)大小 if (dictSize(d) < count) count = dictSize(d); // 最大次數(shù) = 次數(shù) * 10 maxsteps = count*10; /* 如果字典在 rehash 中, 嘗試 count 一樣次數(shù)的 rehash */ for (j = 0; j < count; j++) { if (dictIsRehashing(d)) _dictRehashStep(d); else break; } // 獲取總的 HashTable 個(gè)數(shù), 如果在 rehash 中就是 2 個(gè), 否則 1 個(gè) tables = dictIsRehashing(d) ? 2 : 1; // 獲取數(shù)組大小的掩碼, 用于計(jì)算索引值 maxsizemask = d->ht[0].sizemask; if (tables > 1 && maxsizemask < d->ht[1].sizemask) maxsizemask = d->ht[1].sizemask; // 隨機(jī)獲取一個(gè)位置 unsigned long i = random() & maxsizemask; unsigned long emptylen = 0; // 獲取到的個(gè)數(shù)沒(méi)達(dá)到需要的個(gè)數(shù) 或者嘗試的次數(shù)還沒(méi)達(dá)到 0 while(stored < count && maxsteps--) { for (j = 0; j < tables; j++) { // 如果字典在 rehash 中, 同時(shí)當(dāng)前處理的是第一個(gè)字典, 處理的位置小于 rehash 下次處理的位置, // 則跳過(guò)這個(gè)位置, 直接到 rehash 下次處理的位置 // 因?yàn)榈谝粋€(gè)字典 rehash 下次處理的位置前的數(shù)據(jù)都遷移到第二個(gè)字典中了 if (tables == 2 && j == 0 && i < (unsigned long) d->rehashidx) { // 防止獲取數(shù)據(jù)的位置 i 超過(guò)第二個(gè)字典的大小 if (i >= d->ht[1].size) i = d->rehashidx; else continue; } // 超過(guò)了數(shù)組的長(zhǎng)度 if (i >= d->ht[j].size) continue; // 獲取對(duì)應(yīng)位置的數(shù)據(jù) dictEntry *he = d->ht[j].table[i]; // 對(duì)應(yīng)的位置為 null if (he == NULL) { emptylen++; // 獲取 null 數(shù)據(jù)的次數(shù)大于 5 次 同時(shí) 大于需要的過(guò)期 key 的個(gè)數(shù) if (emptylen >= 5 && emptylen > count) { // 重新計(jì)算獲取的位置 i, 重新獲取 i = random() & maxsizemask; emptylen = 0; } } else { emptylen = 0; while (he) { // he 本身是鏈表, 計(jì)算從鏈表中獲取到的個(gè)數(shù), 夠了結(jié)束, 不夠就 i+1, 從字典的下一個(gè)位置繼續(xù)獲取 *des = he; des++; he = he->next; stored++; if (stored == count) return stored; } } } i = (i+1) & maxsizemask; } return stored; }
dictGetSomeKeys 函數(shù)簡(jiǎn)單理解就是, 通過(guò) random() 得到一個(gè)隨機(jī)數(shù), 這個(gè)隨機(jī)數(shù) & 數(shù)組大小的掩碼, 得到一個(gè)位置, 從這個(gè)位置向后獲取 count 個(gè)過(guò)期 key。
這個(gè)處理的過(guò)程中
有可能字典在 rehash 中, 數(shù)據(jù)分布在 2 個(gè)字典中, 所以有時(shí)第一個(gè)字典獲取不到需要到第二個(gè)字典獲取
需要的過(guò)期 key 的個(gè)數(shù)小于等于 5 個(gè), 通過(guò)計(jì)算得到的位置獲取到的數(shù)據(jù)連續(xù)都為 null, 則重新通過(guò) random() 計(jì)算一個(gè)新的位置
為了防止長(zhǎng)時(shí)間的需要, 在外面還計(jì)算了最大的循環(huán)次數(shù)
從上面的代碼實(shí)現(xiàn)可以看出, Redis 內(nèi)部對(duì) LRU + LFU 的實(shí)現(xiàn)都是不是很正式的實(shí)現(xiàn), 帶有一定的誤差和隨機(jī)性。
其本身考慮主用是從性能上做的折中。比如傳統(tǒng)的 LRU 算法, 需要將所有的數(shù)據(jù)維護(hù)一個(gè)雙向鏈表
訪問(wèn)節(jié)點(diǎn), 如果節(jié)點(diǎn)存在, 則將該節(jié)點(diǎn)移動(dòng)到鏈表的頭節(jié)點(diǎn), 并返回節(jié)點(diǎn)值, 不存在就返回 null
新增節(jié)點(diǎn), 節(jié)點(diǎn)不存在, 就在鏈表的頭部新增節(jié)點(diǎn), 如果節(jié)點(diǎn)存在, 則更新節(jié)點(diǎn)數(shù)據(jù), 然后將節(jié)點(diǎn)移動(dòng)到鏈表的頭節(jié)點(diǎn)
需要消耗的內(nèi)存在維護(hù)鏈表的 + 節(jié)點(diǎn)的挑戰(zhàn), 對(duì)于一個(gè)大規(guī)模的數(shù)據(jù), 這個(gè)消耗是非常大的。
所以 Redis 采用了其思想, 通過(guò)另外的方式達(dá)到類(lèi)似的效果。
4 附錄: Redis 幾個(gè)對(duì)象的介紹
4.1 Redis 中的字典
4.2.1 HashTable
存儲(chǔ)在 Redis 中的基本都是鍵值對(duì), 而這種鍵值對(duì)存儲(chǔ), 同時(shí)可以通過(guò) key 快速查詢到對(duì)應(yīng)的 value, 最合適的實(shí)現(xiàn)就是 HashTable 了。
而實(shí)現(xiàn) HashTable 的底層結(jié)構(gòu),基本就是一個(gè)數(shù)組或者鏈表, 同時(shí)為了解決 hash 沖突, 數(shù)組或鏈表的每個(gè)節(jié)點(diǎn)定義為一個(gè)鏈表。
Redis 中對(duì) HashTable 的實(shí)現(xiàn)也是如此, 大體如下
Redis 中實(shí)現(xiàn)的 HastTable 叫做 dictht (Dictionary Hash Table)
對(duì)應(yīng)的定義如下:
typedef struct dictht { // 存放節(jié)點(diǎn)的數(shù)組 dictEntry **table; // HashTable 的大小, 2 的冪次方 unsigned long size; // HashTable 的大小掩碼, 用于計(jì)算索引值 unsigned long sizemask; // HashTable 中已經(jīng)使用的節(jié)點(diǎn)個(gè)數(shù) unsigned long used; } dictht;
真實(shí)存儲(chǔ)數(shù)據(jù)的鏈表節(jié)點(diǎn)的定義如下:
typedef struct dictEntry { // 存儲(chǔ)的鍵值對(duì)的 key void *key; // 存儲(chǔ)的鍵值對(duì)的 value union { void *val; uint64_t u64; int64_t s64; double d; } v; // 指向下一個(gè)節(jié)點(diǎn) struct dictEntry *next; } dictEntry;
key + v(value) + next 一個(gè)簡(jiǎn)單的鏈表定義。
有點(diǎn)特殊的就是對(duì)應(yīng)著 value 屬性的 v 的定義是一個(gè)聯(lián)合體, 會(huì)在不同場(chǎng)景下使用不同的字段,
比如一個(gè)鍵值對(duì)的過(guò)期時(shí)間就存放在 s64 中, 這個(gè) value 存放的值就放在 val 中。
一個(gè) dictEntry 的字段存放內(nèi)容大體如下:
4.2.2 字典
在使用 HashTable 時(shí), 都需要提前聲明好容量, 而隨著程序的運(yùn)行, 存放到 HashTable 的數(shù)據(jù)會(huì)越來(lái)越多, 最終達(dá)到上限, 這時(shí)就需要進(jìn)行擴(kuò)容了。
在 Java 的 HashMap 的擴(kuò)容過(guò)程
創(chuàng)建一個(gè)更大容量的數(shù)組
將 HashMap 中舊數(shù)組一次性遷移到新的數(shù)組中
清除掉舊數(shù)組
這個(gè)擴(kuò)容沒(méi)多大問(wèn)題, 但是放到 Redis 中合適嗎?
Redis 是一個(gè)存內(nèi)存的數(shù)據(jù)庫(kù), 所有的數(shù)據(jù)都存放在內(nèi)存中, 基本是 GB 級(jí)別的數(shù)據(jù)量, 每次擴(kuò)容遷移的數(shù)據(jù)量很多
Redis 是一個(gè)單線程的數(shù)據(jù)庫(kù), 一次只能處理一個(gè)事情, 如果全力在做擴(kuò)容, 那么其他的請(qǐng)求將無(wú)法處理
所以 Redis 采用了一種 漸進(jìn)式 rehash 的方法解決擴(kuò)容縮容的問(wèn)題, 過(guò)程如下
維護(hù) 2 個(gè) dictht, 一個(gè)是真實(shí)存儲(chǔ)數(shù)據(jù)的 HashTable A, 一個(gè)是擴(kuò)容后存儲(chǔ)數(shù)據(jù)的 TableTable B + 一個(gè) rehash 位置的索引, 初始值為 0
在 rehash >=0 期間, 每次對(duì) HashTable 進(jìn)行操作, 除了正常的操作外, 還會(huì)將 A rehash 位置的數(shù)據(jù)都遷移到 B, 然后 rehash + 1
隨著對(duì) HashTable 的不斷操作, 最終 A 中的數(shù)據(jù)都會(huì)遷移到 B, 這時(shí)將 rehash 設(shè)置為 -1
基于上面的漸進(jìn)式 rehash 分析, 實(shí)際是需要 2 個(gè) dictht, 所以 Redis 在此至上多封裝了一層
typedef struct dict { dictType *type; void *privdata; dictht ht[2]; // 2 個(gè) HashTable long rehashidx; // rehash 的索引 unsigned long iterators; } dict;
這個(gè)就是 Redis 中的字典, 用于存儲(chǔ)鍵值對(duì)的結(jié)構(gòu)。
在將這個(gè)結(jié)構(gòu)放到一個(gè) redisDb 就是我們常見(jiàn)的 Redis 數(shù)據(jù)庫(kù)了
typedef struct redisDb { dict *dict; dict *expires; .... } redisDb;
redisDb 就是我們常說(shuō)的 Redis 16 個(gè)數(shù)據(jù)庫(kù)的定義了。 每個(gè)數(shù)據(jù)庫(kù)中都有 2 個(gè)字典
dict 正常的字典, 存儲(chǔ)沒(méi)有設(shè)置過(guò)期時(shí)間的鍵值對(duì)
expires 過(guò)期字典, 存儲(chǔ)設(shè)置了過(guò)期時(shí)間的鍵值對(duì)
4.2 Redis 的內(nèi)存待淘汰池
struct evictionPoolEntry { unsigned long long idle; // 對(duì)象空閑時(shí)間 (使用的算法是 LFU 則是逆頻率) sds key; // 待淘汰的鍵值對(duì)的 key sds cached; // 緩存的 key 名稱(chēng) SDS 對(duì)象 int dbid; // 待淘汰鍵值對(duì)的 key 所在的數(shù)據(jù)庫(kù) ID };
5 參考
Redis內(nèi)存兜底策略——內(nèi)存淘汰及回收機(jī)制
到此這篇關(guān)于深入理解Redis內(nèi)存回收和內(nèi)存淘汰機(jī)制的文章就介紹到這了,更多相關(guān)Redis內(nèi)存回收和內(nèi)存淘汰機(jī)制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis高效率原因及數(shù)據(jù)結(jié)構(gòu)分析
這篇文章主要為大家詳細(xì)的介紹了Redis高效的原因以及分析了Redis高效的數(shù)據(jù)結(jié)構(gòu),有需要的朋友可以借鑒參考下,希望能夠有所幫助2021-09-09Redis實(shí)現(xiàn)布隆過(guò)濾器的代碼詳解
布隆過(guò)濾器(Bloom?Filter)是Redis?4.0版本提供的新功能,它被作為插件加載到Redis服務(wù)器中,給Redis提供強(qiáng)大的去重功能,本文將給大家詳細(xì)介紹一下Redis布隆過(guò)濾器,文中有相關(guān)的代碼示例,需要的朋友可以參考下2023-07-07讓Redis在你的系統(tǒng)中發(fā)揮更大作用的幾點(diǎn)建議
Redis在很多方面與其他數(shù)據(jù)庫(kù)解決方案不同:它使用內(nèi)存提供主存儲(chǔ)支持,而僅使用硬盤(pán)做持久性的存儲(chǔ);它的數(shù)據(jù)模型非常獨(dú)特,用的是單線程。另一個(gè)大區(qū)別在于,你可以在開(kāi)發(fā)環(huán)境中使用Redis的功能,但卻不需要轉(zhuǎn)到Redis2014-06-06Redis分布式鎖Redlock的實(shí)現(xiàn)
本文主要介紹了Redis分布式鎖Redlock的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08Redis過(guò)期數(shù)據(jù)是否會(huì)被立馬刪除
這篇文章主要為大家介紹了Redis過(guò)期數(shù)據(jù)會(huì)被立馬刪除么的問(wèn)題解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07