(四)Redis 快取應用、淘汰機制

冬先生發表於2024-06-20

1、快取應用

一個系統中不同層面資料訪問速度不一樣,以計算機為例,CPU、記憶體和磁碟這三層的訪問速度從幾十 ns 到 100ns,再到幾 ms,效能的差異很大,如果每次 CPU 處理資料時都要到磁碟讀取資料,系統執行速度會大大降低。
所以,計算機系統中,預設有兩種快取:
(1)CPU 裡面的末級快取,即 LLC,用來快取記憶體中的資料,避免每次從記憶體中存取資料。
(2)記憶體中的高速頁快取,即 page cache,用來快取磁碟中的資料,避免每次從磁碟中存取資料。
在一個層次化的系統中,快取一定是一個快速子系統,資料存在快取中時,能避免每次從慢速子系統中存取資料。對應到網際網路應用來說,Redis 就是快速子系統,而資料庫就是慢速子系統了。

Redis 是一個獨立的系統軟體,如果應用程式想使用 Redis 快取,就需要增加相應的程式碼。所以,我們也把 Redis 稱為旁路快取,也就是說,讀取快取、讀取資料庫和更新快取的操作都需要在應用程式中來完成。

Redis 快取按照是否接受寫請求,分為只讀快取和讀寫快取兩種型別,只讀快取能加速讀請求,而讀寫快取可以同時加速讀寫請求。讀寫快取又分為同步直寫和非同步寫回,可以根據業務需求在保證效能和保證資料可靠性之間進行選擇。

2、淘汰機制

快取的容量終究是有限的,需要按一定規則淘汰出去,為新來的資料騰出空間,提高快取命中率,提升應用的訪問效能。快取容量的規劃通常是需要結合應用資料實際訪問特徵和成本開銷來綜合考慮的,建議把快取容量設定為總資料量的 15% 到 30%,兼顧訪問效能和記憶體空間開銷。設定容量命令(如4gb):CONFIG SET maxmemory 4gb

8種淘汰策略:noeviction、volatile-random、volatile-ttl、volatile-lru、volatile-lfu、allkeys-lru、allkeys-random、allkeys-lfu
大體分為兩類,noeviction(不淘汰資料),快取被寫滿了,再有寫請求時 Redis 不再提供服務,直接返回錯誤。另外7種是一類,按照一定範圍對快取資料進行淘汰,對設定過期時間的資料進行淘汰,和對所有資料進行淘汰。分類如圖:

具體策略如下:
(1)volatile-ttl: 根據過期時間的先後進行刪除,越早過期的越先被刪除。
(2)volatile-rando: 在設定了過期時間的鍵值對中,進行隨機刪除。
(3)volatile-lru: 使用 LRU 演算法篩選設定了過期時間的鍵值對。
(4)volatile-lfu: 使用 LFU 演算法選擇設定了過期時間的鍵值對。
(5)allkeys-random: 從所有鍵值對中隨機選擇並刪除資料。
(6)allkeys-lru: 使用 LRU 演算法在所有資料中進行篩選。
(7)allkeys-lfu: 使用 LFU 演算法在所有資料中進行篩選。

LRU 演算法

LRU 演算法全稱 Least Recently Used,按照最近最少使用的原則來篩選資料,最不常用的資料會被篩選出來,而最近頻繁使用的資料會留在快取中。LRU 會把所有的資料組織成一個連結串列,連結串列的頭和尾分別表示 MRU 端和 LRU 端,分別代表最近最常使用的資料和最近最不常用的資料。

舉個例子:資料 20 和 3 被訪問後,它們在連結串列中的位置移動到了 MRU 端,LRU 演算法選擇刪除資料時,都是從 LRU 端開始,所以當新資料15被寫入時,LRU 端的資料5被刪除。LRU 演算法在實際實現時,需要用連結串列管理所有的快取資料,這會帶來額外的空間開銷。而且,當有資料被訪問時,需要在連結串列上把該資料移動到 MRU 端,如果有大量資料被訪問,就會帶來很多連結串列移動操作,會很耗時,進而會降低 Redis 快取效能。

所以,在 Redis 中,LRU 演算法被做了簡化,以減輕資料淘汰對快取效能的影響,具體實現原理是 Redis 預設會記錄每個資料的最近一次訪問的時間戳(由鍵值對資料結構 RedisObject 中的 lru 欄位記錄),在需要選擇淘汰的資料時,Redis首先會隨機選擇N個資料將它們作為一個候選集合,然後比較他們的lru欄位,將lru欄位最小的資料淘汰掉。N 可以透過命令設定:CONFIG SET maxmemory-samples 100

當再次淘汰時,Redis會再挑選一些lru欄位比候選集合中最小lru欄位還要小的鍵值對,將它們放入候選集,如果候選集的資料的個數達到了 maxmemory-sample 配置的個數,Redis就開始將lru欄位值最小的資料淘汰

LFU 演算法

與 LRU 策略相比,LFU 策略中會從兩個維度來篩選並淘汰資料:一是,資料訪問的時效性(訪問時間離當前時間的遠近);二是,資料的被訪問次數。就是在 LRU 策略基礎上,為每個資料增加了一個計數器,來統計訪問次數。淘汰資料時,首先會根據資料的訪問次數進行篩選,把訪問次數最低的資料淘汰出快取。如果兩個資料的訪問次數相同,再比較這兩個資料的訪問時效性,把距離上一次訪問時間更久的資料淘汰出快取。

具體實現是把原來 24bit 大小的 lru 欄位,又進一步拆分成了兩部分:ldt 值(lru 欄位的前 16bit,表示資料的訪問時間戳)、counter 值(lru 欄位的後 8bit,表示資料的訪問次數)。但是 counter 只有 8bit,記錄的最大值是 255,顯然不能因對資料成千上萬次的訪問。實際 LFU 策略實現時,資料訪問並不是簡單的 counter 值加 1 的計數規則,而是採用了一個更最佳化的計數規則。

每當資料被訪問一次時,首先,用計數器當前的值乘以配置項 lfu_log_factor 再加 1,再取其倒數,得到一個 p 值;然後,把這個 p 值和一個取值範圍在(0,1)間的隨機數 r 值比大小,只有 p 值大於 r 值時,計數器才加 1,透過設定不同的 lfu_log_factor 配置項,來控制計數器值增加的速度。以下是計算方式部分程式碼(baseval當前值)和 lfu_log_factor 設定不同值的變化情況:

double r = (double)rand()/RAND_MAX;
...
double p = 1.0/(baseval*server.lfu_log_factor+1);
if (r < p) counter++;   

正是因為使用了非線性遞增的計數器方法,即使快取資料的訪問次數成千上萬,LFU 策略也可以有效地區分不同的訪問次數,從而進行合理的資料篩選。從剛才的表中,我們可以看到,當 lfu_log_factor 取值為 10 時,百、千、十萬級別的訪問次數對應的 counter 值已經有明顯的區分了,所以,我們在應用 LFU 策略時,一般可以將 lfu_log_factor 取值為 10。

有些資料在短時間內被大量訪問後就不會再被訪問了,按訪問次數篩選時,這些資料會被留存在快取中,但不會提升快取命中率。為此,Redis 在實現 LFU 策略時,還設計了一個 counter 值的衰減機制。透過配置衰減因子 lfu_decay_time 來控制訪問次數的衰減。

具體操作是計算當前時間和資料最近一次訪問時間的差值,換算成分鐘單位,再除以 lfu_decay_time 值,就是資料 counter 要衰減的值。lfu_decay_time 值越大,相應的衰減值會變小,衰減效果也會減弱。所以,如果業務應用中有短時高頻訪問的資料的話,建議把 lfu_decay_time 值設定為 1,它們不再被訪問後,會較快地衰減它們的訪問次數,儘早把它們從快取中淘汰出去,避免快取汙染。

相關文章