1. 程式人生 > 資料庫 >《redis深度歷險》十-LRU\LFU

《redis深度歷險》十-LRU\LFU

LFU VS LRU

LFU:least frequntly used,按最近的訪問頻率淘汰,比LRU更加精準的表示了一個key的被訪問熱度。

redis物件的熱度

redis的所有物件頭結構中都有一個24bit的欄位,用來記錄物件的熱度。

typedef struct redisObject {
	unsigned type:4; // 物件型別如 zset,set,hash等
	unsigned encoding:4 // 物件編碼如ziplist,intset,skiplist等
	unsigned lru:24; // 物件的熱度
	int refcount; // 引用計數
	void *ptr // 物件的body
}robj

LRU

Least Recently Used

在 LRU 模式下, lru 欄位儲存的是 Redis 時鐘 server.lruclocko Redis 時鐘是一個24bit 的整數,預設是 Unix 時間戳對 2^24 取模的結果,大約 97 天清零一次 。當某個 key被訪問一次,它的物件頭結構的 lru欄位值就會被更新為 server.lruclock。

如果 server.lruclock 沒有折返(對 2^24 取模〉,它就是一直遞增的,這意昧著對 象的 lru欄位不會超過 server.lruclock的值。下方左圖

如果超過了,說明 server.lruclock折返了。 通過這個邏輯就可以精準計算出物件多長時間沒有被訪問一一即“物件的空閒時間”。( 下方右圖有些問題,lru不可能超過max的,小於等於max)

LFU

在LFU模式下,lru欄位用來儲存兩個值,分別是ldt(last decrement time)16bit和logc(logistic counter)8bit

loge 是 8 個 bit,用來儲存訪問頻次,因為 8 個 bit 能表示的最大整數值為 255,儲存頻次肯定遠遠不夠,所以這 8 個 bit 儲存的是頻次的對數值,並且這個 值還會隨時間衰減,如果它的值比較小,那麼就很容易被回收。為了確保新建立的 物件不被回收,新物件的這 8 個 bit 會被初始化為一個大於零的值 LFU_INIT_VAL(預設是=5) 。

ldt 是 16 個 bit,用來儲存上一次 loge 的更新時間

。因為只有 16 個 bit,所以 精度不可能很高。它取的是分鐘時間戳對 i6 進行取模,大約每隔 45 天就會折返。 下呈現了折返前後空閒時間的不同計算規則。同 LRU 模式一樣,我們也可以 使用這個邏輯計算出物件的空閒時間,只不過精度是分鐘級別的。圖中的 server.unixtime 是當前 Redis 記錄的系統時間戳,和 server.lruclock 一樣,它也是每毫秒 更新一次。

ldt不是在物件被訪問時更新的,而是在redis的淘汰邏輯進行時更新的,每次淘汰都是採用隨機策略, 隨機挑選若干個 key,更新這個 key 的“熱度”,淘汰掉“熱度”最低的 key。因為 Redis 採用的是隨機演算法,如果 key 比較多的話,那麼 ldt 更新得可能會比較慢。不 過既然它是分鐘級別的精度,也沒有必要更新得過於頻繁。

ldt 更新的同時也會一同衰減 loge 的值。衰減也有特定的演算法。

總而言之,就是通過logc和ldt相互結合,可以獲得某個鍵的使用頻率。

快取時間戳

redis每一次獲取系統時 間戳都是一次系統呼叫, 系統呼叫相對來說是比較費時間的,作為單執行緒的Redis承 受不起,所以它需要對時間進行快取,獲取時間都是從快取中直接拿。

惰性刪除

共享機制

127.0.0.1:6379> sadd heat wade bosh james
(integer) 3
127.0.0.1:6379> sadd cavs irving love james
(integer) 3
127.0.0.1:6379> sunionstore players heat cavs
(integer) 5
127.0.0.1:6379> smembers players
1) "wade"
2) "bosh"
3) "james"
4) "love"
5) "irving"

新的集合包含了舊集合的所有元素,裡面有一個trick,底層的字串物件被共享了。

為什麼物件共享是懶惰刪除的最大障礙呢,因為懶惰刪除相當於徹底砍掉某個樹枝,將它扔到非同步刪除佇列裡去,這裡是徹底刪除,如果底層的物件是被共享的,就不是徹底刪除,下圖就不是徹底刪除。

為了支援懶惰刪除,就必須實現無共享設計。

非同步刪除的實現

主執行緒將刪除任務傳遞給非同步執行緒,他是通過一個普通的雙向連結串列實現的,因為連結串列需要支援多執行緒併發操作,所以需要有鎖來保護。

執行懶惰刪除時, Redis 將刪除操作的相關引數封裝成一個 bio_job 結構,然後 追加到連結串列尾部。非同步執行緒通過遍歷連結串列摘取 job 元素來挨個執行非同步任務 。

佇列安全

前面提到任務佇列是個不安全的雙向連結串列 , 需要使用鎖來保護它。當主執行緒 將任務追加到佇列之前需要給它加鎖,追加完畢後,再釋放鎖,還需要喚醒非同步線 程一一 如果其在休眠的話。

非同步執行緒需要對任務佇列進行輪詢處理,依次從連結串列表頭摘取元素逐個處理 。 摘取元素的時候也需要加鎖,摘出來之後再解鎖。如果一個元素都沒有,它需要等待, 直到主執行緒來喚醒它繼續工作。