數(shù)據(jù)存儲(chǔ)和有效期
在 redis
工作流程中,過期的數(shù)據(jù)并不需要馬上就要執(zhí)行刪除操作。因?yàn)檫@些刪不刪除只是一種狀態(tài)表示,可以異步
的去處理,在不忙的時(shí)候去把這些不緊急的刪除操作做了,從而保證 redis
的高效
數(shù)據(jù)的存儲(chǔ)
在redis中數(shù)據(jù)的存儲(chǔ)不僅僅需要保存數(shù)據(jù)本身還要保存數(shù)據(jù)的生命周期,也就是過期時(shí)間。在redis 中 數(shù)據(jù)的存儲(chǔ)結(jié)構(gòu)如下圖:
![](/d/20211018/fa1d188d29bf7bf85772d9b90962f183.gif)
獲取有效期
Redis是一種內(nèi)存級(jí)數(shù)據(jù)庫,所有數(shù)據(jù)均存放在內(nèi)存中,內(nèi)存中的數(shù)據(jù)可以通過TTL指令獲取其狀態(tài)
![](/d/20211018/418c5de5c9b2d1867ede9c049f1a8107.gif)
刪除策略
在內(nèi)存占用與CPU占用之間尋找一種平衡,顧此失彼都會(huì)造成整體redis性能的下降,甚至引發(fā)服務(wù)器宕機(jī)或內(nèi)存泄漏。
定時(shí)刪除
創(chuàng)建一個(gè)定時(shí)器,當(dāng)key設(shè)置過期時(shí)間,且過期時(shí)間到達(dá)時(shí),由定時(shí)器任務(wù)立即執(zhí)行對(duì)鍵的刪除操作
優(yōu)點(diǎn)
節(jié)約內(nèi)存,到時(shí)就刪除,快速釋放掉不必要的內(nèi)存占用
缺點(diǎn)
CPU壓力很大,無論CPU此時(shí)負(fù)載多高,均占用CPU,會(huì)影響redis服務(wù)器響應(yīng)時(shí)間和指令吞吐量
總結(jié)
用處理器性能換取存儲(chǔ)空間
惰性刪除
數(shù)據(jù)到達(dá)過期時(shí)間,不做處理。等下次訪問該數(shù)據(jù),如果未過期,返回?cái)?shù)據(jù)。發(fā)現(xiàn)已經(jīng)過期,刪除,返回不存在。這樣每次讀寫數(shù)據(jù)都需要檢測(cè)數(shù)據(jù)是否已經(jīng)到達(dá)過期時(shí)間。也就是惰性刪除總是在數(shù)據(jù)的讀寫時(shí)發(fā)生的。
expireIfNeeded函數(shù)
對(duì)所有的讀寫命令進(jìn)行檢查,檢查操作的對(duì)象是否過期。過期就刪除返回過期,不過期就什么也不做~。
執(zhí)行數(shù)據(jù)寫入過程中,首先通過expireIfNeeded函數(shù)對(duì)寫入的key進(jìn)行過期判斷。
/*
* 為執(zhí)行寫入操作而取出鍵 key 在數(shù)據(jù)庫 db 中的值。
*
* 和 lookupKeyRead 不同,這個(gè)函數(shù)不會(huì)更新服務(wù)器的命中/不命中信息。
*
* 找到時(shí)返回值對(duì)象,沒找到返回 NULL 。
*/
robj *lookupKeyWrite(redisDb *db, robj *key) {
// 刪除過期鍵
expireIfNeeded(db,key);
// 查找并返回 key 的值對(duì)象
return lookupKey(db,key);
}
執(zhí)行數(shù)據(jù)讀取過程中,首先通過expireIfNeeded函數(shù)對(duì)寫入的key進(jìn)行過期判斷。
/*
* 為執(zhí)行讀取操作而取出鍵 key 在數(shù)據(jù)庫 db 中的值。
*
* 并根據(jù)是否成功找到值,更新服務(wù)器的命中/不命中信息。
*
* 找到時(shí)返回值對(duì)象,沒找到返回 NULL 。
*/
robj *lookupKeyRead(redisDb *db, robj *key) {
robj *val;
// 檢查 key 釋放已經(jīng)過期
expireIfNeeded(db,key);
// 從數(shù)據(jù)庫中取出鍵的值
val = lookupKey(db,key);
// 更新命中/不命中信息
if (val == NULL)
server.stat_keyspace_misses++;
else
server.stat_keyspace_hits++;
// 返回值
return val;
}
執(zhí)行過期動(dòng)作expireIfNeeded其實(shí)內(nèi)部做了三件事情,分別是:
- 查看key判斷是否過期
- 向slave節(jié)點(diǎn)傳播執(zhí)行過期key的動(dòng)作并發(fā)送事件通知
- 刪除過期key
/*
* 檢查 key 是否已經(jīng)過期,如果是的話,將它從數(shù)據(jù)庫中刪除。
*
* 返回 0 表示鍵沒有過期時(shí)間,或者鍵未過期。
*
* 返回 1 表示鍵已經(jīng)因?yàn)檫^期而被刪除了。
*/
int expireIfNeeded(redisDb *db, robj *key) {
// 取出鍵的過期時(shí)間
mstime_t when = getExpire(db,key);
mstime_t now;
// 沒有過期時(shí)間
if (when 0) return 0; /* No expire for this key */
/* Don't expire anything while loading. It will be done later. */
// 如果服務(wù)器正在進(jìn)行載入,那么不進(jìn)行任何過期檢查
if (server.loading) return 0;
// 當(dāng)服務(wù)器運(yùn)行在 replication 模式時(shí)
// 附屬節(jié)點(diǎn)并不主動(dòng)刪除 key
// 它只返回一個(gè)邏輯上正確的返回值
// 真正的刪除操作要等待主節(jié)點(diǎn)發(fā)來刪除命令時(shí)才執(zhí)行
// 從而保證數(shù)據(jù)的同步
if (server.masterhost != NULL) return now > when;
// 運(yùn)行到這里,表示鍵帶有過期時(shí)間,并且服務(wù)器為主節(jié)點(diǎn)
/* Return when this key has not expired */
// 如果未過期,返回 0
if (now = when) return 0;
/* Delete the key */
server.stat_expiredkeys++;
// 向 AOF 文件和附屬節(jié)點(diǎn)傳播過期信息
propagateExpire(db,key);
// 發(fā)送事件通知
notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
"expired",key,db->id);
// 將過期鍵從數(shù)據(jù)庫中刪除
return dbDelete(db,key);
}
判斷key是否過期的數(shù)據(jù)結(jié)構(gòu)是db->expires,也就是通過expires的數(shù)據(jù)結(jié)構(gòu)判斷數(shù)據(jù)是否過期。
內(nèi)部獲取過期時(shí)間并返回。
/*
* 返回字典中包含鍵 key 的節(jié)點(diǎn)
*
* 找到返回節(jié)點(diǎn),找不到返回 NULL
*
* T = O(1)
*/
dictEntry *dictFind(dict *d, const void *key)
{
dictEntry *he;
unsigned int h, idx, table;
// 字典(的哈希表)為空
if (d->ht[0].size == 0) return NULL; /* We don't have a table at all */
// 如果條件允許的話,進(jìn)行單步 rehash
if (dictIsRehashing(d)) _dictRehashStep(d);
// 計(jì)算鍵的哈希值
h = dictHashKey(d, key);
// 在字典的哈希表中查找這個(gè)鍵
// T = O(1)
for (table = 0; table = 1; table++) {
// 計(jì)算索引值
idx = h d->ht[table].sizemask;
// 遍歷給定索引上的鏈表的所有節(jié)點(diǎn),查找 key
he = d->ht[table].table[idx];
// T = O(1)
while(he) {
if (dictCompareKeys(d, key, he->key))
return he;
he = he->next;
}
// 如果程序遍歷完 0 號(hào)哈希表,仍然沒找到指定的鍵的節(jié)點(diǎn)
// 那么程序會(huì)檢查字典是否在進(jìn)行 rehash ,
// 然后才決定是直接返回 NULL ,還是繼續(xù)查找 1 號(hào)哈希表
if (!dictIsRehashing(d)) return NULL;
}
// 進(jìn)行到這里時(shí),說明兩個(gè)哈希表都沒找到
return NULL;
}
優(yōu)點(diǎn)
節(jié)約CPU性能,發(fā)現(xiàn)必須刪除的時(shí)候才刪除。
缺點(diǎn)
內(nèi)存壓力很大,出現(xiàn)長(zhǎng)期占用內(nèi)存的數(shù)據(jù)。
總結(jié)
用存儲(chǔ)空間換取處理器性能
定期刪除
周期性輪詢r(jià)edis庫中時(shí)效性數(shù)據(jù),采用隨機(jī)抽取的策略,利用過期數(shù)據(jù)占比的方式刪除頻度。
優(yōu)點(diǎn)
CPU性能占用設(shè)置有峰值,檢測(cè)頻度可自定義設(shè)置
內(nèi)存壓力不是很大,長(zhǎng)期占用內(nèi)存的冷數(shù)據(jù)會(huì)被持續(xù)清理
缺點(diǎn)
需要周期性抽查存儲(chǔ)空間
定期刪除詳解
redis的定期刪除是通過定時(shí)任務(wù)實(shí)現(xiàn)的,也就是定時(shí)任務(wù)會(huì)循環(huán)調(diào)用serverCron
方法。然后定時(shí)檢查過期數(shù)據(jù)的方法是databasesCron
。定期刪除的一大特點(diǎn)就是考慮了定時(shí)刪除過期數(shù)據(jù)會(huì)占用cpu時(shí)間,所以每次執(zhí)行databasesCron
的時(shí)候會(huì)限制cpu的占用不超過25%。真正執(zhí)行刪除的是 activeExpireCycle
方法。
時(shí)間事件
對(duì)于持續(xù)運(yùn)行的服務(wù)器來說, 服務(wù)器需要定期對(duì)自身的資源和狀態(tài)進(jìn)行必要的檢查和整理, 從而讓服務(wù)器維持在一個(gè)健康穩(wěn)定的狀態(tài), 這類操作被統(tǒng)稱為常規(guī)操作(cron job)
在 Redis 中, 常規(guī)操作由 redis.c/serverCron()
實(shí)現(xiàn), 它主要執(zhí)行以下操作
1 更新服務(wù)器的各類統(tǒng)計(jì)信息,比如時(shí)間、內(nèi)存占用、數(shù)據(jù)庫占用情況等。
2 清理數(shù)據(jù)庫中的過期鍵值對(duì)。
3 對(duì)不合理的數(shù)據(jù)庫進(jìn)行大小調(diào)整。
4 關(guān)閉和清理連接失效的客戶端。
5 嘗試進(jìn)行 AOF 或 RDB 持久化操作。
6 如果服務(wù)器是主節(jié)點(diǎn)的話,對(duì)附屬節(jié)點(diǎn)進(jìn)行定期同步。
7 如果處于集群模式的話,對(duì)集群進(jìn)行定期同步和連接測(cè)試。
因?yàn)?serverCron()
需要在 Redis 服務(wù)器運(yùn)行期間一直定期運(yùn)行, 所以它是一個(gè)循環(huán)時(shí)間事件: serverCron()
會(huì)一直定期執(zhí)行,直到服務(wù)器關(guān)閉為止。
在 Redis 2.6 版本中, 程序規(guī)定 serverCron()
每秒運(yùn)行 10
次, 平均每 100
毫秒運(yùn)行一次。 從 Redis 2.8 開始, 用戶可以通過修改 hz
選項(xiàng)來調(diào)整 serverCron()
的每秒執(zhí)行次數(shù), 具體信息請(qǐng)參考 redis.conf
文件中關(guān)于 hz
選項(xiàng)的說明
查看hz
way1 : config get hz # "hz" "10"
way2 : info server # server.hz 10
serverCron()
serverCron()
會(huì)定期的執(zhí)行,在serverCron()
執(zhí)行中會(huì)調(diào)用databasesCron()
方法(serverCron()
還做了其他很多事情,但是現(xiàn)在不討論,只談刪除策略)
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
// 略去多無關(guān)代碼
/* We need to do a few operations on clients asynchronously. */
// 檢查客戶端,關(guān)閉超時(shí)客戶端,并釋放客戶端多余的緩沖區(qū)
clientsCron();
/* Handle background operations on Redis databases. */
// 對(duì)數(shù)據(jù)庫執(zhí)行各種操作
databasesCron(); /* !我們關(guān)注的方法! */
databasesCron()
在 databasesCron()
中 調(diào)用了 activeExpireCycle()
方法,來對(duì)過期的數(shù)據(jù)進(jìn)行處理。(在這里還會(huì)做一些其他操作~ 調(diào)整數(shù)據(jù)庫大小,主動(dòng)和漸進(jìn)式rehash)
// 對(duì)數(shù)據(jù)庫執(zhí)行刪除過期鍵,調(diào)整大小,以及主動(dòng)和漸進(jìn)式 rehash
void databasesCron(void) {
// 判斷是否是主服務(wù)器 如果是 執(zhí)行主動(dòng)過期鍵清除
if (server.active_expire_enabled server.masterhost == NULL)
// 清除模式為 CYCLE_SLOW ,這個(gè)模式會(huì)盡量多清除過期鍵
activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
// 在沒有 BGSAVE 或者 BGREWRITEAOF 執(zhí)行時(shí),對(duì)哈希表進(jìn)行 rehash
if (server.rdb_child_pid == -1 server.aof_child_pid == -1) {
static unsigned int resize_db = 0;
static unsigned int rehash_db = 0;
unsigned int dbs_per_call = REDIS_DBCRON_DBS_PER_CALL;
unsigned int j;
/* Don't test more DBs than we have. */
// 設(shè)定要測(cè)試的數(shù)據(jù)庫數(shù)量
if (dbs_per_call > server.dbnum) dbs_per_call = server.dbnum;
/* Resize */
// 調(diào)整字典的大小
for (j = 0; j dbs_per_call; j++) {
tryResizeHashTables(resize_db % server.dbnum);
resize_db++;
}
/* Rehash */
// 對(duì)字典進(jìn)行漸進(jìn)式 rehash
if (server.activerehashing) {
for (j = 0; j dbs_per_call; j++) {
int work_done = incrementallyRehash(rehash_db % server.dbnum);
rehash_db++;
if (work_done) {
/* If the function did some work, stop here, we'll do
* more at the next cron loop. */
break;
}
}
}
}
}
activeExpireCycle()
大致流程如下
1 遍歷指定個(gè)數(shù)的db(默認(rèn)的 16 )進(jìn)行刪除操作
2 針對(duì)每個(gè)db隨機(jī)獲取過期數(shù)據(jù)每次遍歷不超過指定數(shù)量(如20),發(fā)現(xiàn)過期數(shù)據(jù)并進(jìn)行刪除。
3 如果有多于25%的keys過期,重復(fù)步驟 2
除了主動(dòng)淘汰的頻率外,Redis對(duì)每次淘汰任務(wù)執(zhí)行的最大時(shí)長(zhǎng)也有一個(gè)限定,這樣保證了每次主動(dòng)淘汰不會(huì)過多阻塞應(yīng)用請(qǐng)求,以下是這個(gè)限定計(jì)算公式:
#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* CPU max % for keys collection */ ``... ``timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
也就是每次執(zhí)行時(shí)間的25%用于過期數(shù)據(jù)刪除。
void activeExpireCycle(int type) {
// 靜態(tài)變量,用來累積函數(shù)連續(xù)執(zhí)行時(shí)的數(shù)據(jù)
static unsigned int current_db = 0; /* Last DB tested. */
static int timelimit_exit = 0; /* Time limit hit in previous call? */
static long long last_fast_cycle = 0; /* When last fast cycle ran. */
unsigned int j, iteration = 0;
// 默認(rèn)每次處理的數(shù)據(jù)庫數(shù)量
unsigned int dbs_per_call = REDIS_DBCRON_DBS_PER_CALL;
// 函數(shù)開始的時(shí)間
long long start = ustime(), timelimit;
// 快速模式
if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
// 如果上次函數(shù)沒有觸發(fā) timelimit_exit ,那么不執(zhí)行處理
if (!timelimit_exit) return;
// 如果距離上次執(zhí)行未夠一定時(shí)間,那么不執(zhí)行處理
if (start last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return;
// 運(yùn)行到這里,說明執(zhí)行快速處理,記錄當(dāng)前時(shí)間
last_fast_cycle = start;
}
/*
* 一般情況下,函數(shù)只處理 REDIS_DBCRON_DBS_PER_CALL 個(gè)數(shù)據(jù)庫,
* 除非:
*
* 1) 當(dāng)前數(shù)據(jù)庫的數(shù)量小于 REDIS_DBCRON_DBS_PER_CALL
* 2) 如果上次處理遇到了時(shí)間上限,那么這次需要對(duì)所有數(shù)據(jù)庫進(jìn)行掃描,
* 這可以避免過多的過期鍵占用空間
*/
if (dbs_per_call > server.dbnum || timelimit_exit)
dbs_per_call = server.dbnum;
// 函數(shù)處理的微秒時(shí)間上限
// ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 默認(rèn)為 25 ,也即是 25 % 的 CPU 時(shí)間
timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
timelimit_exit = 0;
if (timelimit = 0) timelimit = 1;
// 如果是運(yùn)行在快速模式之下
// 那么最多只能運(yùn)行 FAST_DURATION 微秒
// 默認(rèn)值為 1000 (微秒)
if (type == ACTIVE_EXPIRE_CYCLE_FAST)
timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */
// 遍歷數(shù)據(jù)庫
for (j = 0; j dbs_per_call; j++) {
int expired;
// 指向要處理的數(shù)據(jù)庫
redisDb *db = server.db+(current_db % server.dbnum);
// 為 DB 計(jì)數(shù)器加一,如果進(jìn)入 do 循環(huán)之后因?yàn)槌瑫r(shí)而跳出
// 那么下次會(huì)直接從下個(gè) DB 開始處理
current_db++;
do {
unsigned long num, slots;
long long now, ttl_sum;
int ttl_samples;
/* If there is nothing to expire try next DB ASAP. */
// 獲取數(shù)據(jù)庫中帶過期時(shí)間的鍵的數(shù)量
// 如果該數(shù)量為 0 ,直接跳過這個(gè)數(shù)據(jù)庫
if ((num = dictSize(db->expires)) == 0) {
db->avg_ttl = 0;
break;
}
// 獲取數(shù)據(jù)庫中鍵值對(duì)的數(shù)量
slots = dictSlots(db->expires);
// 當(dāng)前時(shí)間
now = mstime();
// 這個(gè)數(shù)據(jù)庫的使用率低于 1% ,掃描起來太費(fèi)力了(大部分都會(huì) MISS)
// 跳過,等待字典收縮程序運(yùn)行
if (num slots > DICT_HT_INITIAL_SIZE
(num*100/slots 1)) break;
/*
* 樣本計(jì)數(shù)器
*/
// 已處理過期鍵計(jì)數(shù)器
expired = 0;
// 鍵的總 TTL 計(jì)數(shù)器
ttl_sum = 0;
// 總共處理的鍵計(jì)數(shù)器
ttl_samples = 0;
// 每次最多只能檢查 LOOKUPS_PER_LOOP 個(gè)鍵
if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;
// 開始遍歷數(shù)據(jù)庫
while (num--) {
dictEntry *de;
long long ttl;
// 從 expires 中隨機(jī)取出一個(gè)帶過期時(shí)間的鍵
if ((de = dictGetRandomKey(db->expires)) == NULL) break;
// 計(jì)算 TTL
ttl = dictGetSignedIntegerVal(de)-now;
// 如果鍵已經(jīng)過期,那么刪除它,并將 expired 計(jì)數(shù)器增一
if (activeExpireCycleTryExpire(db,de,now)) expired++;
if (ttl 0) ttl = 0;
// 累積鍵的 TTL
ttl_sum += ttl;
// 累積處理鍵的個(gè)數(shù)
ttl_samples++;
}
/* Update the average TTL stats for this database. */
// 為這個(gè)數(shù)據(jù)庫更新平均 TTL 統(tǒng)計(jì)數(shù)據(jù)
if (ttl_samples) {
// 計(jì)算當(dāng)前平均值
long long avg_ttl = ttl_sum/ttl_samples;
// 如果這是第一次設(shè)置數(shù)據(jù)庫平均 TTL ,那么進(jìn)行初始化
if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
/* Smooth the value averaging with the previous one. */
// 取數(shù)據(jù)庫的上次平均 TTL 和今次平均 TTL 的平均值
db->avg_ttl = (db->avg_ttl+avg_ttl)/2;
}
// 我們不能用太長(zhǎng)時(shí)間處理過期鍵,
// 所以這個(gè)函數(shù)執(zhí)行一定時(shí)間之后就要返回
// 更新遍歷次數(shù)
iteration++;
// 每遍歷 16 次執(zhí)行一次
if ((iteration 0xf) == 0 /* check once every 16 iterations. */
(ustime()-start) > timelimit)
{
// 如果遍歷次數(shù)正好是 16 的倍數(shù)
// 并且遍歷的時(shí)間超過了 timelimit
// 那么斷開 timelimit_exit
timelimit_exit = 1;
}
// 已經(jīng)超時(shí)了,返回
if (timelimit_exit) return;
// 如果已刪除的過期鍵占當(dāng)前總數(shù)據(jù)庫帶過期時(shí)間的鍵數(shù)量的 25 %
// 那么不再遍歷
} while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
}
}
hz調(diào)大將會(huì)提高Redis主動(dòng)淘汰的頻率,如果你的Redis存儲(chǔ)中包含很多冷數(shù)據(jù)占用內(nèi)存過大的話,可以考慮將這個(gè)值調(diào)大,但Redis作者建議這個(gè)值不要超過100。我們實(shí)際線上將這個(gè)值調(diào)大到100,觀察到CPU會(huì)增加2%左右,但對(duì)冷數(shù)據(jù)的內(nèi)存釋放速度確實(shí)有明顯的提高(通過觀察keyspace個(gè)數(shù)和used_memory大?。?/p>
可以看出timelimit和server.hz是一個(gè)倒數(shù)的關(guān)系,也就是說hz配置越大,timelimit就越小。換句話說是每秒鐘期望的主動(dòng)淘汰頻率越高,則每次淘汰最長(zhǎng)占用時(shí)間就越短。這里每秒鐘的最長(zhǎng)淘汰占用時(shí)間是固定的250ms(1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/100),而淘汰頻率和每次淘汰的最長(zhǎng)時(shí)間是通過hz參數(shù)控制的。
因此當(dāng)redis中的過期key比率沒有超過25%之前,提高h(yuǎn)z可以明顯提高掃描key的最小個(gè)數(shù)。假設(shè)hz為10,則一秒內(nèi)最少掃描200個(gè)key(一秒調(diào)用10次*每次最少隨機(jī)取出20個(gè)key),如果hz改為100,則一秒內(nèi)最少掃描2000個(gè)key;另一方面,如果過期key比率超過25%,則掃描key的個(gè)數(shù)無上限,但是cpu時(shí)間每秒鐘最多占用250ms。
當(dāng)REDIS運(yùn)行在主從模式時(shí),只有主結(jié)點(diǎn)才會(huì)執(zhí)行上述這兩種過期刪除策略,然后把刪除操作”del key”同步到從結(jié)點(diǎn)。
if (server.active_expire_enabled server.masterhost == NULL) // 判斷是否是主節(jié)點(diǎn) 從節(jié)點(diǎn)不需要執(zhí)行activeExpireCycle()函數(shù)。
// 清除模式為 CYCLE_SLOW ,這個(gè)模式會(huì)盡量多清除過期鍵
activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
隨機(jī)個(gè)數(shù)
redis.config.ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 決定每次循環(huán)從數(shù)據(jù)庫 expire中隨機(jī)挑選值的個(gè)數(shù)
逐出算法
如果不限制 reids 對(duì)內(nèi)存使用的限制,它將會(huì)使用全部的內(nèi)存。可以通過 config.memory
來指定redis 對(duì)內(nèi)存的使用量 。
下面是redis 配置文件中的說明
543 # Set a memory usage limit to the specified amount of bytes.
544 # When the memory limit is reached Redis will try to remove keys
545 # according to the eviction policy selected (see maxmemory-policy).
546 #
547 # If Redis can't remove keys according to the policy, or if the policy is
548 # set to 'noeviction', Redis will start to reply with errors to commands
549 # that would use more memory, like SET, LPUSH, and so on, and will continue
550 # to reply to read-only commands like GET.
551 #
552 # This option is usually useful when using Redis as an LRU or LFU cache, or to
553 # set a hard memory limit for an instance (using the 'noeviction' policy).
554 #
555 # WARNING: If you have replicas attached to an instance with maxmemory on,
556 # the size of the output buffers needed to feed the replicas are subtracted
557 # from the used memory count, so that network problems / resyncs will
558 # not trigger a loop where keys are evicted, and in turn the output
559 # buffer of replicas is full with DELs of keys evicted triggering the deletion
560 # of more keys, and so forth until the database is completely emptied.
561 #
562 # In short... if you have replicas attached it is suggested that you set a lower
563 # limit for maxmemory so that there is some free RAM on the system for replica
564 # output buffers (but this is not needed if the policy is 'noeviction').
將內(nèi)存使用限制設(shè)置為指定的字節(jié)。當(dāng)已達(dá)到內(nèi)存限制Redis將根據(jù)所選的逐出策略(請(qǐng)參閱maxmemory策略)嘗試刪除數(shù)據(jù)。
如果Redis無法根據(jù)逐出策略移除密鑰,或者策略設(shè)置為“noeviction”,Redis將開始對(duì)使用更多內(nèi)存的命令(如set、LPUSH等)進(jìn)行錯(cuò)誤回復(fù),并將繼續(xù)回復(fù)只讀命令,如GET。
當(dāng)將Redis用作LRU或LFU緩存或設(shè)置實(shí)例的硬內(nèi)存限制(使用“noeviction”策略)時(shí),此選項(xiàng)通常很有用。
警告:如果將副本附加到啟用maxmemory的實(shí)例,則將從已用內(nèi)存計(jì)數(shù)中減去饋送副本所需的輸出緩沖區(qū)的大小,這樣,網(wǎng)絡(luò)問題/重新同步將不會(huì)觸發(fā)收回密鑰的循環(huán),而副本的輸出緩沖區(qū)將充滿收回的密鑰增量,從而觸發(fā)刪除更多鍵,依此類推,直到數(shù)據(jù)庫完全清空。
簡(jiǎn)而言之。。。如果附加了副本,建議您設(shè)置maxmemory的下限,以便系統(tǒng)上有一些空閑RAM用于副本輸出緩沖區(qū)(但如果策略為“noeviction”,則不需要此限制)。
驅(qū)逐策略的配置
Maxmemery-policy volatile-lru
當(dāng)前已用內(nèi)存超過 maxmemory
限定時(shí),觸發(fā)主動(dòng)清理策略
易失數(shù)據(jù)清理
volatile-lru:只對(duì)設(shè)置了過期時(shí)間的key進(jìn)行LRU(默認(rèn)值)
volatile-random:隨機(jī)刪除即將過期key
volatile-ttl : 刪除即將過期的
volatile-lfu:挑選最近使用次數(shù)最少的數(shù)據(jù)淘汰
全部數(shù)據(jù)清理
allkeys-lru : 刪除lru算法的key
allkeys-lfu:挑選最近使用次數(shù)最少的數(shù)據(jù)淘汰
allkeys-random:隨機(jī)刪除
禁止驅(qū)逐
(Redis 4.0 默認(rèn)策略)
noeviction : 永不過期,返回錯(cuò)誤當(dāng)mem_used內(nèi)存已經(jīng)超過maxmemory的設(shè)定,對(duì)于所有的讀寫請(qǐng)求都會(huì)觸發(fā)redis.c/freeMemoryIfNeeded(void)
函數(shù)以清理超出的內(nèi)存。注意這個(gè)清理過程是阻塞的,直到清理出足夠的內(nèi)存空間。所以如果在達(dá)到maxmemory并且調(diào)用方還在不斷寫入的情況下,可能會(huì)反復(fù)觸發(fā)主動(dòng)清理策略,導(dǎo)致請(qǐng)求會(huì)有一定的延遲。
清理時(shí)會(huì)根據(jù)用戶配置的maxmemory-policy來做適當(dāng)?shù)那謇恚ㄒ话闶荓RU或TTL),這里的LRU或TTL策略并不是針對(duì)redis的所有key,而是以配置文件中的maxmemory-samples個(gè)key作為樣本池進(jìn)行抽樣清理。
maxmemory-samples在redis-3.0.0中的默認(rèn)配置為5,如果增加,會(huì)提高LRU或TTL的精準(zhǔn)度,redis作者測(cè)試的結(jié)果是當(dāng)這個(gè)配置為10時(shí)已經(jīng)非常接近全量LRU的精準(zhǔn)度了,并且增加maxmemory-samples會(huì)導(dǎo)致在主動(dòng)清理時(shí)消耗更多的CPU時(shí)間,建議:
1 盡量不要觸發(fā)maxmemory,最好在mem_used內(nèi)存占用達(dá)到maxmemory的一定比例后,需要考慮調(diào)大hz以加快淘汰,或者進(jìn)行集群擴(kuò)容。
2 如果能夠控制住內(nèi)存,則可以不用修改maxmemory-samples配置;如果Redis本身就作為L(zhǎng)RU cache服務(wù)(這種服務(wù)一般長(zhǎng)時(shí)間處于maxmemory狀態(tài),由Redis自動(dòng)做LRU淘汰),可以適當(dāng)調(diào)大maxmemory-samples。
這里提一句,實(shí)際上redis根本就不會(huì)準(zhǔn)確的將整個(gè)數(shù)據(jù)庫中最久未被使用的鍵刪除,而是每次從數(shù)據(jù)庫中隨機(jī)取5個(gè)鍵并刪除這5個(gè)鍵里最久未被使用的鍵。上面提到的所有的隨機(jī)的操作實(shí)際上都是這樣的,這個(gè)5可以用過redis的配置文件中的maxmemeory-samples參數(shù)配置。
數(shù)據(jù)逐出策略配置依據(jù)
使用INFO命令輸出監(jiān)控信息,查詢緩存int和miss的次數(shù),根據(jù)業(yè)務(wù)需求調(diào)優(yōu)Redis配置。
總結(jié)
到此這篇關(guān)于redis 數(shù)據(jù)刪除策略和逐出算法的問題小結(jié)的文章就介紹到這了,更多相關(guān)redis 刪除策略 逐出算法內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
您可能感興趣的文章:- 如何基于js及java分析并封裝排序算法
- 詳解vue3.0 diff算法的使用(超詳細(xì))
- 詳細(xì)分析JAVA加解密算法
- SHA:安全散列算法簡(jiǎn)析 附實(shí)例
- Python實(shí)現(xiàn)ElGamal加密算法的示例代碼
- python實(shí)現(xiàn)mean-shift聚類算法
- 經(jīng)典實(shí)例講解C#遞歸算法
- 通過代碼實(shí)例了解頁面置換算法原理