Redis系列 一文搞懂快取淘汰策略

2021-10-10 11:16:34 字數 3378 閱讀 1495

在使用redis時,我們一般會為redis的快取空間設定乙個大小,不會讓資料無限制的放入redis快取。

對於 redis 來說,一旦確定了快取最大容量,比如 4gb,你就可以使用下面這個命令來設定快取的大小了:

config set maxmemory 4gb

redis設定了快取的容量大小,那麼快取被寫滿是不可避免的。我們需要面對快取寫滿時的替換操作。快取替換需要解決兩個問題:決定淘汰哪些資料,如何處理那些被淘汰的資料。

通常情況下推薦優先使用 allkeys-lru 策略。這樣,可以充分利用 lru 這一經典快取演算法的優勢,把最近最常訪問的資料留在快取中,提公升應用的訪問效能。如果你的業務資料中有明顯的冷熱資料區分,我建議你使用 allkeys-lru 策略。如果業務應用中的資料訪問頻率相差不大,沒有明顯的冷熱資料區分,建議使用 allkeys-random 策略,隨機選擇淘汰的資料就行。

lru 演算法的全稱是 least recently used,從名字上就可以看出,這是按照最近最少使用的原則來篩選資料,最不常用的資料會被篩選出來,而最近頻繁使用的資料會留在快取中。

lru 會把所有的資料組織成乙個鍊錶,鍊錶的頭和尾分別表示 mru 端和 lru 端,分別代表最近最常使用的資料和最近最不常用的資料。我們看乙個例子。

如果有乙個新資料 45 要被寫入快取,但此時已經沒有快取空間了,也就是鍊錶沒有空餘位置了,那麼,lru 演算法做兩件事:資料 45 是剛被訪問的,所以它會被放到 mru 端;演算法把 lru 端的資料 5 從快取中刪除,相應的鍊錶中就沒有資料 5 的記錄了。

lru認為剛剛被訪問的資料,肯定還會被再次訪問,所以就把它放在 mru 端;長久不訪問的資料,肯定就不會再被訪問了,所以就讓它逐漸後移到 lru 端,在快取滿時,就優先刪除它。

lru 演算法在實際實現時,需要用鍊錶管理所有的快取資料,這會帶來額外的空間開銷。而且,當有資料被訪問時,需要在鍊錶上把該資料移動到 mru 端,如果有大量資料被訪問,就會帶來很多鍊錶移動操作,會很耗時,進而會降低 redis 快取效能。

所以,在 redis 中,lru 演算法被做了簡化,以減輕資料淘汰對快取效能的影響。具體來說,redis 缺省會記錄每個資料的最近一次訪問的時間戳(由鍵值對資料結構 redisobject 中的 lru 字段記錄)。然後,redis 在決定淘汰的資料時,第一次會隨機選出 n 個資料,把它們作為乙個候選集合。接下來,redis 會比較這 n 個資料的 lru 字段,把 lru 字段值最小的資料從快取中淘汰出去。

redis 提供了乙個配置引數 maxmemory-samples,這個引數就是 redis 選出的資料個數 n。例如,我們執行如下命令,可以讓 redis 選出 100 個資料作為候選資料集:

config set maxmemory-samples 100

當需要再次淘汰資料時,redis 需要挑選資料進入第一次淘汰時建立的候選集合。這兒的挑選標準是:能進入候選集合的資料的 lru 字段值必須小於候選集合中最小的 lru 值。當有新資料進入候選資料集後,如果候選資料集中的資料個數達到了 maxmemory-samples,redis 就把候選資料集中 lru 字段值最小的資料淘汰出去。這樣一來,redis 快取不用為所有的資料維護乙個大煉表,也不用在每次資料訪問時都移動煉表項,提公升了快取的效能。

lfu是在redis4.0後出現的,lru的最近最少使用實際上並不精確,考慮下面的情況,如果在|處刪除,那麼a距離的時間最久,但實際上a的使用頻率要比b頻繁,所以合理的淘汰策略應該是淘汰b。lfu就是為應對這種情況而生的。

lfu (least frequently used) :最近最少使用,跟使用的次數有關,淘汰使用次數最少的。

lru (least recently used): 最近最不經常使用,跟使用的最後一次時間有關,淘汰最近使用時間離現在最久的。

~~~~~a~~~~~a~~~~~a~~~~a~~~~~a~~~~~a~~|

~~r~~r~~r~~r~~r~~r~~r~~r~~r~~r~~r~~r~|

~~~~~~~~~~c~~~~~~~~~c~~~~~~~~~c~~~~~~|

~~~~~d~~~~~~~~~~d~~~~~~~~~d~~~~~~~~~d|

每個波浪號代表一秒,a 每五秒,r 每兩秒,c 和 d 每十秒 (如果不是等寬字型,看上去長度可能不一致,實際字元數是一樣的), 最近被訪問的字元是 d,但顯然按照現有的規律,下乙個被訪問的更可能是 r 而不是 d。

lru 實現上比較簡單,最簡單的只需要 鍊錶和map 就夠了,移除元素時直接從鍊錶隊尾移除,增加時加到頭部就可以了。

但實際上 redis 的實現並不是這樣, redis 的實現非常直接,幾乎就是 lru 本身的意思。redis 本身有全域性的時鐘 server.lruclock(單位為秒,24位,190多天會溢位),然後隨機取樣 n 個 key 的訪問時間,離現在最久的,淘汰之。

redis 物件(簡單理解為乙個 key-value)定義如下:

typedef struct redisobject  robj;
lfu 實現更為複雜,需要考慮幾個問題:

如果實現為鍊錶,當物件被訪問時按訪問次數移動到鍊錶的某個有序位置可能是低效的,因為可能存在大量訪問次數相同的 key,最差情況是o(n) (鍊錶無法直接用二分查詢,可以用 跳表?)

某些 key 訪問次數可能非常之大,理論上可以無限大,但實際上我們並不需要精確的訪問次數

訪問次數特別大的 key 可能以後都不再訪問了,但是因為訪問次數大而一直占用著記憶體不被淘汰,需要乙個方法來逐步「驅除」(有點 lru的意思),最簡單的就是逐步衰減訪問次數

本著能省則省的原則,redis 只用了 24bit (server.lruclock 也是24bit)來記錄上述的資訊,是的不是 24byte,連32位指標都放不下!

16bit : 上一次遞減時間 (解決第三個問題)

8bit : 訪問次數 (解決第二個問題)

訪問次數的計算如下:

uint8_t lfulogincr(uint8_t counter)
核心就是訪問次數越大,訪問次數被遞增的可能性越小,最大 255,此外你可以在配置 redis.conf 中寫明訪問多少次遞增多少。

由於訪問次數是有限的,所以第乙個問題也被解決了,直接乙個255陣列或鍊錶都可以。

16bit 部分怎麼用呢?儲存的是時間戳的後16位(分鐘),表示上一次遞減的時間,演算法是這樣執行,隨機取樣n個key(與原來的版本一樣),檢查遞減時間,如果距離現在超過 n 分鐘(可配置),則遞減或者減半(如果訪問次數數值比較大)。

此外,由於新加入的 key 訪問次數很可能比不被訪問的老 key小,為了不被馬上淘汰,新key訪問次數設為 5。

一文搞懂transform skew

目錄 如何理解斜切 skew,先看乙個 demo。在下面的 demo 中,有 4 個正方形,分別是 紅色 不做 skew 變換,綠色 x 方向變換,藍色 y 方向變換,黑色 兩個方向都變換,拖動下面的滑塊可以檢視改變 skew 角度後的效果。切換 selector 可以設定 transform or...

一文搞懂property函式

接下來我帶大家了解乙個函式的作用以及使用技巧,希望對大家都有幫助,話不多說,接下來就開始我的表演特性 首先property有兩種用法,一種是作為函式的用法,一種是作為裝飾器的用法,接下來我們就逐一分析 property函式 看一下作為函式它包含的引數都有哪些 property fget none,f...

一文搞懂記憶體屏障

gcc編譯選項中有個 o選項,表示編譯 的時候進行優化。這樣就會出現一種可能 優化後的 和優化前的 順序不一致。來看個例子 8 include9 10 int a,b 11 12 int main 13 很簡單,我們使用不加優化選項來將其編譯為組合語言 yuhao laplace workspace...