記憶體淘汰演算法 Redis資料淘汰演算法

2021-10-14 16:13:13 字數 4652 閱讀 1894

眾所周知,redis的所有資料都儲存在記憶體中,但是記憶體是一種有限的資源,所以為了防止redis無限制的使用記憶體,在啟動redis時可以通過配置項maxmemory來指定其最大能使用的記憶體容量。例如可以通過以下配置來設定redis最大能使用 1g 記憶體:

maxmemory 1g
當redis使用的記憶體超過配置的maxmemory時,便會觸發資料淘汰策略。redis提供了多種資料淘汰的策略,如下:

可以在啟動redis時,通過配置項maxmemory_policy來指定要使用的資料淘汰策略。例如要使用volatile-lru策略可以通過以下配置來指定:

maxmemory_policy volatile-lru
lru是least recently used的縮寫,即最近最少使用,很多快取系統都使用此演算法作為淘汰策略。

最簡單的實現方式就是把所有快取通過乙個鍊錶連線起來,新建立的快取新增到鍊錶的頭部,如果有快取被訪問了,就把快取移動到鍊錶的頭部。由於被訪問的快取會移動到鍊錶的頭部,所以沒有被訪問的快取會隨著時間的推移移動的鍊錶的尾部,淘汰資料時只需要從鍊錶的尾部開始即可。下圖展示了這個過程:

redis使用了結構體robj來儲存快取物件,而robj結構有個名為lru的字段,用於記錄快取物件最後被訪問的時間,redis就是以lru欄位的值作為淘汰依據。robj結構如下:

typedef struct redisobject  robj;
當快取物件被訪問時,便會更新此字段的值。**如下:

robj *lookupkey(redisdb *db, robj *key, int flags)  else 

}return val;

} else

}

lookupkey()函式用於查詢key對應的快取物件,所以當快取物件被訪問時便會呼叫此函式。

接下來我們分析一下當redis記憶體使用超過配置的最大記憶體使用限制時的處理方式。

redis在處理每乙個命令時都會檢查記憶體的使用是否超過了限制的最大值,處理命令是通過processcommand()函式進行的,檢查記憶體使用情況的**如下:

int processcommand(client *c) 

}...

}

檢查記憶體的使用情況主要通過freememoryifneededandsafe()函式進行,而freememoryifneededandsafe()函式最終會呼叫freememoryifneeded()函式進行處理,由於freememoryifneeded()函式比較龐大,所以我們分段來進行分析:

int freememoryifneeded(void) 

}if (!total_keys) break; /* no keys to evict. */for (k = evpool_size-1; k >= 0; k--) else if (pool[k].key != pool[k].cached)sdsfree(pool[k].key);

pool[k].key = null;

pool[k].idle = 0;if (de) else }}

}

如果記憶體使用總量超出限制,並且配置了淘汰策略,那麼就開始資料淘汰過程。在上面的**中,mem_tofree變數表示要淘汰的資料總量,而mem_freed變數表示已經淘汰的資料總量。所以在while迴圈中的條件是mem_freed < mem_tofree,表示淘汰的資料總量一定要達到mem_tofree為止。

前面介紹過,redis的淘汰策略有很多中,所以進行資料淘汰時需要根據配置的策略進行。如果配置的淘汰策略是lru/lfu/ttl的話,那麼就進入if**塊。在if**塊裡,首先呼叫evictionpoolpopulate()函式選擇一些快取物件樣本放置到evictionpoollru陣列中。evictionpoolpopulate()函式後面會進行分析,現在只需要知道evictionpoolpopulate()函式是選取一些快取物件樣本就可以了。

獲取到快取物件樣本後,還需要從樣本中獲取最合適的快取物件進行淘汰,因為在選擇樣本時會把最合適的快取物件放置在evictionpoollru陣列的尾部,所以只需要從evictionpoollru陣列的尾部開始查詢乙個不為空的快取物件即可。

else if (server.maxmemory_policy == maxmemory_allkeys_random ||

server.maxmemory_policy == maxmemory_volatile_random)

}}

如果使用隨機淘汰策略,那麼就進入else if**塊,這部分**的邏輯很簡單,如果配置的淘汰策略是volatile-random,那麼就從有過期時間的快取物件中隨機獲取,否則就從所有的快取物件中隨機獲取。

if (bestkey) 

}}

如果找到要淘汰的快取物件,那麼就開始釋放快取物件所占用的記憶體空間。除了需要釋放快取物件占用的記憶體空間外,還需要進行一些其他的操作,比如把淘汰的快取物件同步到從伺服器和把淘汰的快取物件追加到aof檔案中等。

當條件mem_freed < mem_tofree為假時便會退出while迴圈,說明redis的記憶體使用總量已經小於最大的記憶體使用限制,freememoryifneeded()函式便會返回c_ok表示成功執行。

前面說了,當使用非隨機淘汰策略時需要進行資料取樣(volatile-lru/volatile-lfu/volatile-ttl/allkeys-lru/allkeys-lfu),資料取樣通過evictionpoolpopulate()函式進行,由於此函式比較龐大,所以對**分段分析:

void evictionpoolpopulate(int dbid, dict *sampledict, dict *keydict, struct evictionpoolentry *pool) if (server.maxmemory_policy & maxmemory_flag_lru)  else if (server.maxmemory_policy & maxmemory_flag_lfu)  else if (server.maxmemory_policy == maxmemory_volatile_ttl)  else
上面的**主要是獲取樣本快取物件的排序權值idel,如果使用lru淘汰演算法,那麼就呼叫estimateobjectidletime()函式獲取排序權值,estimateobjectidletime()函式用於獲取快取物件有多長時間沒有被訪問。排序按照idle的值公升序排序,就是說idle的值越大,就排到越後。

k = 0;while (k < evpool_size &&

pool[k].key &&

pool[k].idle < idle) k++;if (k == 0 && pool[evpool_size-1].key != null) else if (k < evpool_size && pool[k].key == null) else else

}int klen = sdslen(key);if (klen > evpool_cached_sds_size) else

pool[k].idle = idle;

pool[k].dbid = dbid;

}}

上面這段**的作用是:根據idle的值找到當前快取物件所在evictionpoollru陣列的位置,然後把快取物件儲存到evictionpoollru陣列中。以下插**釋了資料取樣的過程:

所以evictionpoollru陣列的最後乙個元素便是最優的淘汰快取物件。

從上面的分析可知,淘汰資料時只是從樣本中找到最優的淘汰快取物件,並不是從所有快取物件集合中查詢。由於前面介紹的lru演算法需要維護乙個lru鍊錶,而維護乙個lru鍊錶的成本比較大,所以redis才出此下策。

Redis 記憶體資料淘汰策略

no eviction 預設策略。禁止驅逐,保證資料不會丟失 allkeys lru 針對所有key,優先刪除最近最少使用 less recently used 的key volatile lru 針對設定了過期時間的key,優先刪除最近最少使用 less recently used 的key al...

Redis 記憶體淘汰機制

摘要redis是一款優秀的 開源的記憶體資料庫,我在閱讀redis原始碼實現的過程中,時時刻刻能感受到redis作者為更好地使用記憶體而費盡各種心思,例如最明顯的是對於同一種資料結構在不同應用場景下提供了基於不同底層編碼的實現 如壓縮列表 跳躍表等 今天我們暫時放下對redis不同資料結構的 來一起...

redis 記憶體淘汰機制

redis記憶體淘汰指的是使用者儲存的一些鍵被可以被redis主動地從例項中刪除,從而產生讀miss的情況,那麼redis為什麼要有這種功能?這就是我們需要 的設計初衷。redis最常見的兩種應用場景為快取和持久儲存,首先要明確的乙個問題是記憶體淘汰策略更適合於那種場景?是持久儲存還是快取?記憶體的...