眾所周知,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最常見的兩種應用場景為快取和持久儲存,首先要明確的乙個問題是記憶體淘汰策略更適合於那種場景?是持久儲存還是快取?記憶體的...