1. 程式人生 > 資料庫 >Redis鍵過期策略

Redis鍵過期策略

1.1 過期檢查方式

定時刪除是集中處理,惰性刪除是零散處理。

redis 會將每個設定了過期時間的 key 放入到一個獨立的字典中,以後會定時遍歷這個字典來刪除到期的 key。

惰性策略

在客戶端訪問這個 key 的時候,redis 對 key 的過期時間進行檢查,如果過期了就立即刪除。

定時掃描策略

Redis 預設會每秒進行十次過期掃描,過期掃描不會遍歷過期字典中所有的 key,而是採用了一種簡單的貪心策略。

1、從過期字典中隨機 20 個 key;

2、刪除這 20 個 key 中已經過期的 key;

3、如果過期的 key 比率超過 1/4,那就重複步驟 1;

同時,為了保證過期掃描不會出現迴圈過度,導致執行緒卡死現象,掃描時間的上限,預設不會超過 25ms。

從庫的過期策略

從庫不會進行過期掃描的。主庫在 key 到期時,會在 AOF檔案裡增加一條 del 指令,同步到所有的從庫,從庫通過執行這條 del 指令來刪除過期的key。

指令同步是非同步進行的,會出現主從資料的不一致,主庫沒有的資料在從庫裡還存在,比如叢集環境分散式鎖的演算法漏洞就是因為這個同步延遲產生的。

1.2 LRU

maxmemory-policy

redis的預設記憶體淘汰策略為noenviction,當實際記憶體超出 maxmemory 時,Redis 提供了幾種可選策略 (maxmemory-policy) 來讓使用者自己決定該如何騰出新的空間以繼續提供讀寫服務。此時應該同步修改 maxmemory 和 maxmemory-policy 引數。

1. noeviction 不會繼續 寫請求 (DEL 請求可以繼續服務),讀請求可以進行。這樣可以保證不會丟失資料,但是會讓線上的業務不能持續進行。這是預設的淘汰策略。

2. volatile-lru 嘗試淘汰設定了過期時間的 key,最少使用的 key 優先被淘汰。沒有設定過期時間的 key 不會被淘汰,這樣可以保證需要持久化的資料不會突然丟失。

3.volatile-ttl 跟上面一樣,除了淘汰的策略不是 LRU,而是 key 的剩餘壽命 ttl 的值,ttl越小越優先被淘汰。

4.volatile-random 跟上面一樣,不過淘汰的 key 是過期 key 集合中隨機的 key。

5.allkeys-lru 區別於 volatile-lru,這個策略要淘汰的 key 物件是全體的 key 集合,而不只是過期的 key 集合。這意味著沒有設定過期時間的 key 也會被淘汰。

6.allkeys-random 跟上面一樣,不過淘汰的策略是隨機的 key。

volatile-xxx 策略只會針對帶過期時間的 key 進行淘汰,allkeys-xxx 策略會對所有的key 進行淘汰。如果你只是拿 Redis 做快取,那應該使用 allkeys-xxx,客戶端寫快取時不必攜帶過期時間。如果你還想同時使用 Redis 的持久化功能,那就使用 volatile-xxx策略,這樣可以保留沒有設定過期時間的 key,它們是永久的 key 不會被 LRU 演算法淘汰。

LRU 演算法

實現 LRU 演算法除了需要 key/value 字典外,附加一個連結串列,連結串列元素按照一定的順序排列。

當空間滿的時候,會踢掉連結串列尾部的元素。

當字典的某個元素被訪問時,它在連結串列中的位置會被移動到表頭。

連結串列尾部元素是不被重用的元素,被踢掉。表頭的元素就是最近剛剛用過的元素,暫時不會被踢。

近似 LRU 演算法

Redis 使用一種近似 LRU 演算法,它跟 LRU 演算法還不太一樣。

原因

LRU演算法需要消耗大量的額外的記憶體,需要對現有的資料結構進行較大的改造。

近似LRU 演算法很簡單,在現有資料結構的基礎上使用隨機取樣法來淘汰元素,能達到和 LRU演算法非常近似的效果。

實現原理

Redis 為實現近似 LRU 演算法,它給每個 key 增加了一個額外的小欄位,長度是 24 個 bit,也就是最後一次被訪問的時間戳。上一節提到處理 key 過期方式分為集中處理和懶惰處理,LRU 淘汰不一樣,它的處理方式只有懶惰處理。當 Redis 執行寫操作時,發現記憶體超出 maxmemory,就會執行一次LRU 淘汰演算法。這個演算法也很簡單,就是隨機取樣出 5(可以配置) 個 key,然後淘汰掉最舊的 key,如果淘汰後記憶體還是超出 maxmemory,那就繼續隨機取樣淘汰,直到記憶體低於maxmemory 為止。

如何取樣就是看 maxmemory-policy 的配置,如果是 allkeys 就是從所有的 key 字典中隨機,如果是 volatile 就從帶過期時間的 key 字典中隨機。每次取樣多少個 key 看的是maxmemory_samples 的配置,預設為 5。

同時 Redis3.0 在演算法中增加了淘汰池,進一步提升了近似 LRU 演算法的效果。淘汰池是一個數組,它的大小是 maxmemory_samples,在每一次淘汰迴圈中,新隨機出來的 key 列表會和淘汰池中的 key 列表進行融合,淘汰掉最舊的一個 key 之後,保留剩餘較舊的 key 列表放入淘汰池中留待下一個迴圈。

1.3 相關函式

freeMemoryIfNeeded

/* 根據當前的maxmemory設定,定期呼叫此函式以檢視是否有可用的記憶體。如果超出了記憶體限制,該函式將嘗試釋放一些記憶體以在該限制下返回*。 如果處於記憶體限制之下或超過了限制,則函式返回C_OK,但是釋放記憶體的嘗試成功了。 如果我們超出了記憶體限制,但是沒有足夠的記憶體被釋放以在該限制下返回,該函式將返回C_ERR。. */

int freeMemoryIfNeeded(void) {

int keys_freed = 0;

// 副本忽略

if (server.masterhost && server.repl_slave_ignore_maxmemory) return C_OK;

size_t mem_reported, mem_tofree, mem_freed;

mstime_t latency, eviction_latency, lazyfree_latency;

long long delta;

int slaves = listLength(server.slaves);

int result = C_ERR;

 

if (clientsArePaused()) return C_OK;

if (getMaxmemoryState(&mem_reported,NULL,&mem_tofree,NULL) == C_OK)

return C_OK;

mem_freed = 0; // 初始化已釋放記憶體的位元組數為 0

 

latencyStartMonitor(latency);

if (server.maxmemory_policy == MAXMEMORY_NO_EVICTION)

goto cant_free; // maxmemory 策略為不淘汰,那麼直接返回

// 遍歷字典,釋放記憶體並記錄被釋放記憶體的位元組數

while (mem_freed < mem_tofree) { //每次迴圈刪除一個節點, 迴圈直到達到水位線以下

int j, k, i;

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)

{

struct evictionPoolEntry *pool = EvictionPoolLRU;

 

while(bestkey == NULL) {

unsigned long total_keys = 0, keys;

 

/* 遍歷所有字典 dbnum為字典總數 */

for (i = 0; i < server.dbnum; i++) {

db = server.db+i;

// 這裡需要先指定要進行刪除的字典是超時字典還是主字典

dict = (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) ?

db->dict : db->expires;

if ((keys = dictSize(dict)) != 0) {

//幫助函式,key過期時用一些條目填充evictionPool。添加了空閒時間小於當前key之一的鍵。

//如果有空閒條目,則始終新增key。按升序插入鍵,因此空閒時間較小的鍵在左側,而空閒時間較長的鍵在右側。

evictionPoolPopulate(i, dict, db->dict, pool);

total_keys += keys;

}

}

if (!total_keys) break; /* No keys to evict. */

 

/* 在eviction_pool找到一個可以刪除的的節點 即退出 . */

for (k = EVPOOL_SIZE-1; k >= 0; k--) {

if (pool[k].key == NULL) continue;

bestdbid = pool[k].dbid;

 

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);

}

 

/* Remove the entry from the pool. */

if (pool[k].key != pool[k].cached)

sdsfree(pool[k].key);

pool[k].key = NULL;

pool[k].idle = 0;

 

/* 顯然優先刪除LRU時間最長的,此時bestkey使我們要刪除的元素 */

if (de) {

bestkey = dictGetKey(de);

break;

} else {

/* Ghost... Iterate again. */

}

}

}

}

 

/* volatile-random and allkeys-random policy */

else if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM ||

server.maxmemory_policy == MAXMEMORY_VOLATILE_RANDOM)

{

/* When evicting a random key, we try to evict a key for

* each DB, so we use the static 'next_db' variable to

* incrementally visit all DBs. */

for (i = 0; i < server.dbnum; i++) {

j = (++next_db) % server.dbnum;

db = server.db+j;

dict = (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM) ?

db->dict : db->expires;

if (dictSize(dict) != 0) {

de = dictGetRandomKey(dict);

bestkey = dictGetKey(de);

bestdbid = j;

break;

}

}

}

 

/* 刪除選擇的key */

if (bestkey) {

db = server.db+bestdbid;

robj *keyobj = createStringObject(bestkey,sdslen(bestkey));

propagateExpire(db,keyobj,server.lazyfree_lazy_eviction);

//計算刪除鍵所釋放的記憶體數量

delta = (long long) zmalloc_used_memory();

latencyStartMonitor(eviction_latency);

if (server.lazyfree_lazy_eviction)

dbAsyncDelete(db,keyobj);

else

dbSyncDelete(db,keyobj);

signalModifiedKey(NULL,db,keyobj);

latencyEndMonitor(eviction_latency);

latencyAddSampleIfNeeded("eviction-del",eviction_latency);

delta -= (long long) zmalloc_used_memory();

mem_freed += delta;

// 對淘汰鍵的計數器增一

server.stat_evictedkeys++;

notifyKeyspaceEvent(NOTIFY_EVICTED, "evicted",

keyobj, db->id);

decrRefCount(keyobj);

keys_freed++;

 

if (slaves) flushSlavesOutputBuffers();

 

if (server.lazyfree_lazy_eviction && !(keys_freed % 16)) {

if (getMaxmemoryState(NULL,NULL,NULL,NULL) == C_OK) {

/* Let's satisfy our stop condition. */

mem_freed = mem_tofree;

}

}

} else {

goto cant_free; /* nothing to free... */

}

}

result = C_OK;

 

cant_free:

....

return result;

}

 

evictionPoolEntry結構

/* 為了提高LRU近似的質量,採用了一組鍵,它們是在freeMemoryIfNeeded()呼叫中淘汰的很好的候選者。

eviciton池中的條目按空閒時間排序,將更大的空閒時間放在右邊(升序)。

當使用LFU策略時,將使用倒序頻率指示而不是空閒時間,因此我們仍會以較大的值退出(*較大的倒序頻率意味著將訪問頻率最低的按鍵退出)。*/

#define EVPOOL_SIZE 16

#define EVPOOL_CACHED_SDS_SIZE 255

struct evictionPoolEntry {

unsigned long long idle; /* 物件空閒時間(LFU的倒頻)*/

sds key; /* Key name. */

sds cached; /* Cached SDS object for key name. */

int dbid; /* Key DB number. */

};

 

evictionPoolPopulate

//以下函式所做的事情就是在sampledict中隨機挑選元素,計算LRU,以升序插入pool中

void evictionPoolPopulate(int dbid, dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) {

int j, k, count;

dictEntry *samples[server.maxmemory_samples];

// 此函式對字典進行取樣,以從隨機位置返回一些鍵。

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.maxmemor2y_policy != MAXMEMORY_VOLATILE_TTL) {

if (sampledict != keydict) de = dictFind(keydict, key);

o = dictGetVal(de);

}

 

/* 根據策略計算空閒時間。僅僅因為程式碼最初處理LRU,就將其稱為"閒置",實際上它只是一個分數,分數越高意味著候選者越好。 */

if (server.maxmemory_policy & MAXMEMORY_FLAG_LRU) {

idle = estimateObjectIdleTime(o); //計算給定物件的閒置時長

} else if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {

/* 當使用LRU策略時,按空閒時間對key進行排序,以便從更長的空閒時間開始使key過期。

但是,當該策略是LFU策略時,有一個頻率估計,並且先驅逐頻率較低的金鑰。因此,在池中,我們使用倒轉頻率減去實際頻率到最大255來放置物件。 */

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()");

}

 

/* 將元素插入池中。(要根據idle找到合適的位置)

首先,找到第一個空桶或第一個填充的空桶,它們的空閒時間小於我們的空閒時間。*/

k = 0;

while (k < EVPOOL_SIZE &&

pool[k].key &&

pool[k].idle < idle) k++; //找到一個可以插入的位置 保證以LRU時間升序排列

if (k == 0 && pool[EVPOOL_SIZE-1].key != NULL) {

/* 不需要插入, LRU時間比裡面最小的還大 */

continue;

} else if (k < EVPOOL_SIZE && pool[k].key == NULL) {

/*插入到空位置。插入之前無需設定。 */

} else {

/* 插入中間。現在k指向第一個元素大於要插入的元素 */

if (pool[EVPOOL_SIZE-1].key == NULL) {

/* 插入以後我們需要向後移動元素 */

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-1處插入 */

k--;

/*將k(包括)左側的所有元素向左移,因此我們丟棄空閒時間較短的元素。. */

sds cached = pool[0].cached; /* Save SDS before overwriting. */

if (pool[0].key != pool[0].cached) sdsfree(pool[0].key);

memmove(pool,pool+1,sizeof(pool[0])*k);

pool[k].cached = cached;

}

}

 

/* 嘗試重用在池條目中分配的快取的SDS字串,因為分配和取消分配該物件的成本很高 */

int klen = sdslen(key);

if (klen > EVPOOL_CACHED_SDS_SIZE) {

pool[k].key = sdsdup(key);

} else {

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;

}

}

 

參考:

<<Redis 深度歷險 :核心原理和 和應用實踐>> ,

https://blog.csdn.net/weixin_43705457/article/details/105087813