Redis作為LRUCache的實現
公有云Redis服務:https://www.aliyun.com/product/kvstore?spm=5176.8142029.388261.37.59zzzj
背景
Redis作為目前最流行的KV記憶體資料庫,也實現了自己的LRU
(Latest Recently Used
)演算法,在記憶體寫滿的時候,依據其進行資料的淘汰。LRU演算法本身的含義,這裡不做贅述,嚴格的LRU演算法,會優先選擇淘汰最久沒有訪問的資料,這種實現也比較簡單,通常是用一個雙向連結串列
+一個雜湊表
來實現O(1)
的淘汰和更新操作。但是,Redis
為了節省記憶體使用,和通常的LRU演算法實現不太一樣,Redis使用了取樣的方法來模擬一個近似LRU
演算法。
下面先給一個圖來直觀的感受一下Redis的近似LRU演算法和嚴格LRU演算法的差異,
圖中深灰色和淺灰色的點表示的key數量正好可以寫滿記憶體,綠色的點表示剛寫入的key,淺灰色的點表示被淘汰的key,深灰色的點表示剩餘的沒有被淘汰的key。
在嚴格LRU演算法下,圖的左上部分,最先寫入的一半的key,被順序淘汰掉了,但是在Redis的近似LRU演算法下,圖的左下部分,可能出現很早之前寫入的key,並沒有被淘汰掉,寫入時間更晚的key反而被淘汰了,但是也沒有出現比較極端的剛剛寫入不久的key就被淘汰的情況。
根據Redis作者的說法,如果訪問Redis的模式呈現冪律分佈
,即通常說的二八分佈,Redis 2.8的近似LRU演算法和嚴格LRU演算法差異不大,下面我們就來看看這個近似LRU演算法是怎麼實現的。
圖的右半部分是Redis 3.0對於近似LRU演算法的優化,後面我們會寫文章再介紹,同時我們的Redis雲服務核心後續也會merge該優化。
Redis LRU演算法實現
Redis 2.8.19中使用了一個全域性的LRU時鐘,server.lruclock
,定義如下,
#define REDIS_LRU_BITS 24
unsigned lruclock:REDIS_LRU_BITS; /* Clock for LRU eviction */
預設的LRU時鐘的解析度是1秒,可以通過改變REDIS_LRU_CLOCK_RESOLUTION
巨集的值來改變,Redis會在serverCron()
中呼叫updateLRUClock
定期的更新LRU時鐘,更新的頻率和hz引數有關,預設為100ms
一次,如下,
#define REDIS_LRU_CLOCK_MAX ((1<<REDIS_LRU_BITS)-1) /* Max value of obj->lru */
#define REDIS_LRU_CLOCK_RESOLUTION 1 /* LRU clock resolution in seconds */
void updateLRUClock(void) {
server.lruclock = (server.unixtime/REDIS_LRU_CLOCK_RESOLUTION) &
REDIS_LRU_CLOCK_MAX;
}
server.unixtime
是系統當前的unix時間戳,當lruclock的值超出REDIS_LRU_CLOCK_MAX時,會從頭開始計算,所以在計算一個key的最長沒有訪問時間時,可能key本身儲存的lru訪問時間會比當前的lrulock還要大,這個時候需要計算額外時間,如下,
/* Given an object returns the min number of seconds the object was never
* requested, using an approximated LRU algorithm. */
unsigned long estimateObjectIdleTime(robj *o) {
if (server.lruclock >= o->lru) {
return (server.lruclock - o->lru) * REDIS_LRU_CLOCK_RESOLUTION;
} else {
return ((REDIS_LRU_CLOCK_MAX - o->lru) + server.lruclock) *
REDIS_LRU_CLOCK_RESOLUTION;
}
}
這樣計算會不會有問題呢?還是有的,即某個key就是很久很久沒有訪問,lruclock從頭開始後,又超過了該key儲存的lru訪問時間,這個時間是多久呢,在現有的lru時鐘1秒解析度下,24bit
可以表示的最長時間大約是194
天,所以一個key如果連續194天沒有訪問了,Redis計算該key的idle時間是有誤的,但是這種情況應該非常罕見。
Redis支援的淘汰策略比較多,這裡只涉及和LRU相關的,
-
volatile-lru
設定了過期時間的key參與近似的lru淘汰策略 -
allkeys-lru
所有的key均參與近似的lru淘汰策略
當進行LRU淘汰時,Redis按如下方式進行的,
......
/* volatile-lru and allkeys-lru policy */
else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
{
for (k = 0; k < server.maxmemory_samples; k++) {
sds thiskey;
long thisval;
robj *o;
de = dictGetRandomKey(dict);
thiskey = dictGetKey(de);
/* When policy is volatile-lru we need an additional lookup
* to locate the real key, as dict is set to db->expires. */
if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
de = dictFind(db->dict, thiskey);
o = dictGetVal(de);
thisval = estimateObjectIdleTime(o);
/* Higher idle time is better candidate for deletion */
if (bestkey == NULL || thisval > bestval) {
bestkey = thiskey;
bestval = thisval;
}
}
}
......
Redis會基於server.maxmemory_samples
配置選取固定數目的key,然後比較它們的lru訪問時間,然後淘汰最近最久沒有訪問的key,maxmemory_samples的值越大,Redis的近似LRU演算法就越接近於嚴格LRU演算法,但是相應消耗也變高,對效能有一定影響,樣本值預設為5。
每個key的lru訪問時間更新比較簡單,但是有一點值得注意,為了避免fork
子程式後額外的記憶體消耗,當進行bgsave
或aof rewrite
時,lru訪問時間是不更新的。
robj *lookupKey(redisDb *db, robj *key) {
dictEntry *de = dictFind(db->dict,key->ptr);
if (de) {
robj *val = dictGetVal(de);
/* Update the access time for the ageing algorithm.
* Don`t do it if we have a saving child, as this will trigger
* a copy on write madness. */
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1)
val->lru = server.lruclock;
return val;
} else {
return NULL;
}
}
總結
如果採用雙向連結串列+hash表的方式來實現嚴格的LRU演算法,初步估計每個key要增加額外32個位元組左右的記憶體消耗,當key數量比較多時,還是會帶來相當可觀的記憶體消耗,Redis使用近似的LRU演算法,每個key只需額外24bit的記憶體空間,節省還是相當的大的。後面我們會介紹redis 3.x中對近似LRU演算法的優化,使用盡量少的記憶體,使Redis的LRU演算法更接近於嚴格LRU,敬請期待。
相關文章
- 使用ReactJS作為Backbone的view實現ReactJSView
- redis 作為快取總結Redis快取
- GO實現Redis:GO實現Redis的AOF持久化(4)GoRedis持久化
- Redis作為快取可能會出現的問題及解決方案Redis快取
- Rb(redis blaster),一個為 redis 實現 non-replicated 分片的 python 庫RedisASTPython
- GO實現Redis:GO實現Redis叢集(5)GoRedis
- Android使用LruCache、DiskLruCache實現圖片快取+圖片瀑布流Android快取
- Redis In Action 筆記(六):使用 Redis 作為應用程式元件Redis筆記元件
- Golang 實現 Redis(6): 實現 pipeline 模式的 redis 客戶端GolangRedis模式客戶端
- 將redis作為windows系統的系統服務RedisWindows
- TiDB 作為 MySQL Slave 實現實時資料同步TiDBMySql
- 為什麼在 Redis 實現 Lua 指令碼事務?Redis指令碼
- redis分散式鎖的實現Redis分散式
- 【Redis面試題】Redis的字串是怎麼實現的?Redis面試題字串
- Redis作為單執行緒 為什麼我用它還是出現了超賣呢?Redis執行緒
- redis的有序集的實現原理Redis
- 配置Redis作為快取(六種淘汰策略)Redis快取
- 基於Redis作為發號器生成短網址Python實踐RedisPython
- 【Redis】利用 Redis 實現分散式鎖Redis分散式
- 理解 LruCache 機制
- LruCache 使用及原理
- LruCache原始碼分析原始碼
- Java Redis系列2 (redis的安裝與使用+redis持久化的實現))JavaRedis持久化
- ERP為平臺 實現協作商務(轉)
- Redis Sentinel實現原理Redis
- 機制與意義:作為數字現實的電子遊戲遊戲
- docker 實現 Redis 的哨兵機制DockerRedis
- Memcached 與 Redis 實現的對比Redis
- memcached與redis實現的對比Redis
- redis加鎖的幾種實現Redis
- Redis面試系列:Redis實現分散式鎖Redis面試分散式
- Redis作為快取自我總結(完全轉載)Redis快取
- redis作為mysql的快取伺服器(讀寫分離)RedisMySql快取伺服器
- 《閒扯Redis七》Redis字典結構的底層實現Redis
- Redis 中的原子操作(3)-使用Redis實現分散式鎖Redis分散式
- 談談LruCache原始碼原始碼
- 轉:LruCache演算法演算法
- 分散式鎖----Redis實現分散式Redis