Redis是一個基於內存的鍵值數據庫,其內存管理是很是重要的。本文內存管理的內容包括:過時鍵的懶性刪除和過時刪除以及內存溢出控制策略。redis
Redis使用 maxmemory 參數限制最大可用內存,默認值爲0,表示無限制。限制內存的目的主要 有:算法
maxmemory 限制的是Redis實際使用的內存量,也就是 used_memory統計項對應的內存。因爲內存碎片率的存在,實際消耗的內存 可能會比maxmemory設置的更大,實際使用時要當心這部份內存溢出。具體Redis 內存監控的內容請查看一文了解 Redis 內存監控和內存消耗。數據庫
Redis默認無限使用服務器內存,爲防止極端狀況下致使系統內存耗 盡,建議全部的Redis進程都要配置maxmemory。 在保證物理內存可用的狀況下,系統中全部Redis實例能夠調整 maxmemory參數來達到自由伸縮內存的目的。緩存
Redis 回收內存大體有兩個機制:一是刪除到達過時時間的鍵值對象;二是當內存達到 maxmemory 時觸發內存移除控制策略,強制刪除選擇出來的鍵值對象。bash
Redis 全部的鍵均可以設置過時屬性,內部保存在過時表中,鍵值表和過時表的結果以下圖所示。當 Redis保存大量的鍵,對每一個鍵都進行精準的過時刪除可能會致使消耗大量的 CPU,會阻塞 Redis 的主線程,拖累 Redis 的性能,所以 Redis 採用惰性刪除和定時任務刪除機制實現過時鍵的內存回收。服務器
惰性刪除是指當客戶端操做帶有超時屬性的鍵時,會檢查是否超過鍵的過時時間,而後會同步或者異步執行刪除操做並返回鍵已通過期。這樣能夠節省 CPU成本考慮,不須要單獨維護過時時間鏈表來處理過時鍵的刪除。dom
過時鍵的惰性刪除策略由 db.c/expireifNeeded 函數實現,全部對數據庫的讀寫命令執行以前都會調用 expireifNeeded 來檢查命令執行的鍵是否過時。若是鍵過時,expireifNeeded 會將過時鍵從鍵值表和過時表中刪除,而後同步或者異步釋放對應對象的空間。源碼展現的時 Redis 4.0 版本。異步
expireIfNeeded 先從過時表中獲取鍵對應的過時時間,若是當前時間已經超過了過時時間(lua腳本執行則有特殊邏輯,詳看代碼註釋),則進入刪除鍵流程。刪除鍵流程主要進行了三件事:函數
int expireIfNeeded(redisDb *db, robj *key) {
// 獲取鍵的過時時間
mstime_t when = getExpire(db,key);
mstime_t now;
// 鍵沒有過時時間
if (when < 0) return 0;
// 實例正在從硬盤 laod 數據,好比說 RDB 或者 AOF
if (server.loading) return 0;
// 當執行lua腳本時,只有鍵在lua一開始執行時
// 就到了過時時間纔算過時,不然在lua執行過程當中不算失效
now = server.lua_caller ? server.lua_time_start : mstime();
// 當本實例是slave時,過時鍵的刪除由master發送過來的
// del 指令控制。可是這個函數仍是將正確的信息返回給調用者。
if (server.masterhost != NULL) return now > when;
// 判斷是否未過時
if (now <= when) return 0;
// 代碼到這裏,說明鍵已通過期,並且須要被刪除
server.stat_expiredkeys++;
// 命令傳播,到 slave 和 AOF
propagateExpire(db,key,server.lazyfree_lazy_expire);
// 鍵空間通知使得客戶端能夠經過訂閱頻道或模式, 來接收那些以某種方式改動了 Redis 數據集的事件。
notifyKeyspaceEvent(NOTIFY_EXPIRED,
"expired",key,db->id);
// 若是是惰性刪除,調用dbAsyncDelete,不然調用 dbSyncDelete
return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
dbSyncDelete(db,key);
}
複製代碼
上圖是寫命令傳播的示意圖,刪除命令的傳播和它一致。propagateExpire 函數先調用 feedAppendOnlyFile 函數將命令同步到 AOF 的緩衝區中,而後調用 replicationFeedSlaves函數將命令同步到全部的 slave 中。Redis 複製的機制能夠查看Redis 複製過程詳解。oop
// 將命令傳遞到slave和AOF緩衝區。maser刪除一個過時鍵時會發送Del命令到全部的slave和AOF緩衝區
void propagateExpire(redisDb *db, robj *key, int lazy) {
robj *argv[2];
// 生成同步的數據
argv[0] = lazy ? shared.unlink : shared.del;
argv[1] = key;
incrRefCount(argv[0]);
incrRefCount(argv[1]);
// 若是開啓了 AOF 則追加到 AOF 緩衝區中
if (server.aof_state != AOF_OFF)
feedAppendOnlyFile(server.delCommand,db->id,argv,2);
// 同步到全部 slave
replicationFeedSlaves(server.slaves,db->id,argv,2);
decrRefCount(argv[0]);
decrRefCount(argv[1]);
}
複製代碼
dbAsyncDelete 函數會先調用 dictDelete 來刪除過時表中的鍵,而後處理鍵值表中的鍵值對象。它會根據值的佔用的空間來選擇是直接釋放值對象,仍是交給 bio 異步釋放值對象。判斷依據就是值的估計大小是否大於 LAZYFREE_THRESHOLD 閾值。鍵對象和 dictEntry 對象則都是直接被釋放。
#define LAZYFREE_THRESHOLD 64
int dbAsyncDelete(redisDb *db, robj *key) {
// 刪除該鍵在過時表中對應的entry
if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);
// unlink 該鍵在鍵值表對應的entry
dictEntry *de = dictUnlink(db->dict,key->ptr);
// 若是該鍵值佔用空間很是小,懶刪除反而效率低。因此只有在必定條件下,纔會異步刪除
if (de) {
robj *val = dictGetVal(de);
size_t free_effort = lazyfreeGetFreeEffort(val);
// 若是釋放這個對象消耗不少,而且值未被共享(refcount == 1)則將其加入到懶刪除列表
if (free_effort > LAZYFREE_THRESHOLD && val->refcount == 1) {
atomicIncr(lazyfree_objects,1);
bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);
dictSetVal(db->dict,de,NULL);
}
}
// 釋放鍵值對,或者只釋放key,而將val設置爲NULL來後續懶刪除
if (de) {
dictFreeUnlinkedEntry(db->dict,de);
// slot 和 key 的映射關係是用於快速定位某個key在哪一個 slot中。
if (server.cluster_enabled) slotToKeyDel(key);
return 1;
} else {
return 0;
}
}
複製代碼
dictUnlink 會將鍵值從鍵值表中刪除,可是卻不釋放 key、val和對應的表entry對象,而是將其直接返回,而後再調用dictFreeUnlinkedEntry進行釋放。dictDelete 是它的兄弟函數,可是會直接釋放相應的對象。兩者底層都經過調用 dictGenericDelete來實現。dbAsyncDelete d的兄弟函數 dbSyncDelete 就是直接調用dictDelete來刪除過時鍵。
void dictFreeUnlinkedEntry(dict *d, dictEntry *he) {
if (he == NULL) return;
// 釋放key對象
dictFreeKey(d, he);
// 釋放值對象,若是它不爲null
dictFreeVal(d, he);
// 釋放 dictEntry 對象
zfree(he);
}
複製代碼
Redis 有本身的 bio 機制,主要是處理 AOF 落盤、懶刪除邏輯和關閉大文件fd。bioCreateBackgroundJob 函數將釋放值對象的 job 加入到隊列中,bioProcessBackgroundJobs會從隊列中取出任務,根據類型進行對應的操做。
void *bioProcessBackgroundJobs(void *arg) {
.....
while(1) {
listNode *ln;
ln = listFirst(bio_jobs[type]);
job = ln->value;
if (type == BIO_CLOSE_FILE) {
close((long)job->arg1);
} else if (type == BIO_AOF_FSYNC) {
aof_fsync((long)job->arg1);
} else if (type == BIO_LAZY_FREE) {
// 根據參數來決定要作什麼。有參數1則要釋放它,有參數2和3是釋放兩個鍵值表
// 過時表,也就是釋放db 只有參數三是釋放跳錶
if (job->arg1)
lazyfreeFreeObjectFromBioThread(job->arg1);
else if (job->arg2 && job->arg3)
lazyfreeFreeDatabaseFromBioThread(job->arg2,job->arg3);
else if (job->arg3)
lazyfreeFreeSlotsMapFromBioThread(job->arg3);
}
zfree(job);
......
}
}
複製代碼
dbSyncDelete 則是直接刪除過時鍵,而且將鍵、值和 DictEntry 對象都釋放。
int dbSyncDelete(redisDb *db, robj *key) {
// 刪除過時表中的entry
if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);
// 刪除鍵值表中的entry
if (dictDelete(db->dict,key->ptr) == DICT_OK) {
// 若是開啓了集羣,則刪除slot 和 key 映射表中key記錄。
if (server.cluster_enabled) slotToKeyDel(key);
return 1;
} else {
return 0;
}
}
複製代碼
可是單獨用這種方式存在內存泄露的問題,當過時鍵一直沒有訪問將沒法獲得及時刪除,從而致使內存不能及時釋放。正由於如此,Redis還提供另外一種定時任 務刪除機制做爲惰性刪除的補充。
Redis 內部維護一個定時任務,默認每秒運行10次(經過配置控制)。定時任務中刪除過時鍵邏輯採用了自適應算法,根據鍵的 過時比例、使用快慢兩種速率模式回收鍵,流程以下圖所示。
按期刪除策略由 expire.c/activeExpireCycle 函數實現。在redis事件驅動的循環中的eventLoop->beforesleep和 週期性操做 databasesCron 都會調用 activeExpireCycle 來處理過時鍵。可是兩者傳入的 type 值不一樣,一個是ACTIVE_EXPIRE_CYCLE_SLOW 另一個是ACTIVE_EXPIRE_CYCLE_FAST。activeExpireCycle 在規定的時間,分屢次遍歷各個數據庫,從 expires 字典中隨機檢查一部分過時鍵的過時時間,刪除其中的過時鍵,相關源碼以下所示。
void activeExpireCycle(int type) {
// 上次檢查的db
static unsigned int current_db = 0;
// 上次檢查的最大執行時間
static int timelimit_exit = 0;
// 上一次快速模式運行時間
static long long last_fast_cycle = 0; /* When last fast cycle ran. */
int j, iteration = 0;
// 每次檢查週期要遍歷的DB數
int dbs_per_call = CRON_DBS_PER_CALL;
long long start = ustime(), timelimit, elapsed;
..... // 一些狀態時不進行檢查,直接返回
// 若是上次週期由於執行達到了最大執行時間而退出,則本次遍歷全部db,不然遍歷db數等於 CRON_DBS_PER_CALL
if (dbs_per_call > server.dbnum || timelimit_exit)
dbs_per_call = server.dbnum;
// 根據ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC計算本次最大執行時間
timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
timelimit_exit = 0;
if (timelimit <= 0) timelimit = 1;
// 若是是快速模式,則最大執行時間爲ACTIVE_EXPIRE_CYCLE_FAST_DURATION
if (type == ACTIVE_EXPIRE_CYCLE_FAST)
timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */
// 採樣記錄
long total_sampled = 0;
long total_expired = 0;
// 依次遍歷 dbs_per_call 個 db
for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
int expired;
redisDb *db = server.db+(current_db % server.dbnum);
// 將db數增長,一遍下一次繼續從這個db開始遍歷
current_db++;
do {
..... // 申明變量和一些狀況下 break
if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;
// 主要循環,在過時表中進行隨機採樣,判斷是否比率大於25%
while (num--) {
dictEntry *de;
long long ttl;
if ((de = dictGetRandomKey(db->expires)) == NULL) break;
ttl = dictGetSignedIntegerVal(de)-now;
// 刪除過時鍵
if (activeExpireCycleTryExpire(db,de,now)) expired++;
if (ttl > 0) {
/* We want the average TTL of keys yet not expired. */
ttl_sum += ttl;
ttl_samples++;
}
total_sampled++;
}
// 記錄過時總數
total_expired += expired;
// 即便有不少鍵要過時,也不阻塞好久,若是執行超過了最大執行時間,則返回
if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */
elapsed = ustime()-start;
if (elapsed > timelimit) {
timelimit_exit = 1;
server.stat_expired_time_cap_reached_count++;
break;
}
}
// 當比率小於25%時返回
} while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
}
.....// 更新一些server的記錄數據
}
複製代碼
activeExpireCycleTryExpire 函數的實現就和 expireIfNeeded 相似,這裏就不贅述了。
int activeExpireCycleTryExpire(redisDb *db, dictEntry *de, long long now) {
long long t = dictGetSignedIntegerVal(de);
if (now > t) {
sds key = dictGetKey(de);
robj *keyobj = createStringObject(key,sdslen(key));
propagateExpire(db,keyobj,server.lazyfree_lazy_expire);
if (server.lazyfree_lazy_expire)
dbAsyncDelete(db,keyobj);
else
dbSyncDelete(db,keyobj);
notifyKeyspaceEvent(NOTIFY_EXPIRED,
"expired",keyobj,db->id);
decrRefCount(keyobj);
server.stat_expiredkeys++;
return 1;
} else {
return 0;
}
}
複製代碼
按期刪除策略的關鍵點就是刪除操做執行的時長和頻率:
當Redis所用內存達到maxmemory上限時會觸發相應的溢出控制策略。 具體策略受maxmemory-policy參數控制,Redis支持6種策略,以下所示:
內存溢出控制策略可使用 config set maxmemory-policy {policy} 語句進行動態配置。Redis 提供了豐富的空間溢出控制策略,咱們能夠根據自身業務須要進行選擇。
當設置 volatile-lru 策略時,保證具備過時屬性的鍵能夠根據 LRU 剔除,而未設置超時的鍵能夠永久保留。還能夠採用allkeys-lru 策略把 Redis 變爲純緩存服務器使用。
當Redis由於內存溢出刪除鍵時,能夠經過執行 info stats 命令查看 evicted_keys 指標找出當前 Redis 服務器已剔除的鍵數量。
每次Redis執行命令時若是設置了maxmemory參數,都會嘗試執行回收 內存操做。當Redis一直工做在內存溢出(used_memory>maxmemory)的狀態下且設置非 noeviction 策略時,會頻繁地觸發回收內存的操做,影響Redis 服務器的性能,這一點千萬要引發注意。