在一些場景下,有些數據被訪問的次數非常少,甚至只會被訪問一次。當這些數據服務完訪問請求后,如果還繼續留存在緩存中的話,就只會白白占用緩存空間。這種情況,就是緩存污染。
如果污染數據很少時,對於系統性能的影響就會很小,但是如果一旦數據量很大時,將會大大占用緩存容量,一旦緩存滿的時候,因為保存在緩存中的數據很少使用,我們再往緩存中寫入新數據時,就需要先把這些數據逐步淘汰出緩存,因為淘汰需要額外的操作數據庫,還有寫入緩存開銷,這樣會影響redis的性能。
要解決緩存污染,很容易想到解決方案,那就是得把不會再被訪問的數據篩選出來並淘汰掉。這樣就不用等到緩存被寫滿以后,再逐一淘汰舊數據之后,才能寫入新數據了。而哪些數據能留存在緩存中,是由緩存的淘汰策略決定的。
在明確知道數據被再次訪問的情況下,volatile-ttl 可以有效避免緩存污染。
lru: LRU 策略的核心思想:如果一個數據剛剛被訪問,那么這個數據肯定是熱數據(判斷熱數據的方式),還會被再次訪問。按照這個核心思想,Redis 中的 LRU 策略,會在每個數據對應的 RedisObject 結構體中設置一個 lru 字段,用來記錄數據的訪問時間戳。在進行數據淘汰時,LRU 策略會在候選數據集中淘汰掉 lru 字段值最小的數據(也就是訪問時間最久的數據)。
但是這種淘汰策略的缺陷是,因為該數據被訪問一次就被提到了Mru端,這樣的話,但是它之后就不會被使用,這樣它從mru端過渡到lru端,需要一定時間,中間會經歷一定量的對於其他數據的請求。相當於在使用 LRU 策略淘汰數據時,這些數據會留存在緩存中很長一段時間,造成緩存污染。同時這些數據的一個特性就是只會被訪問一次。所以這里就要引入LFU淘汰策略
LFU 策略中會從兩個維度來篩選並淘汰數據:一是,數據訪問的時效性(訪問時間離當前時間的遠近)(這一點與lru相同);二是,數據的被訪問次數。
LFU 緩存策略是在 LRU 策略基礎上,為每個數據增加了一個計數器,來統計這個數據的訪問次數。當使用 LFU 策略篩選淘汰數據時,首先會根據數據的訪問次數進行篩選,把訪問次數最低的數據淘汰出緩存。如果兩個數據的訪問次數相同,LFU 策略再比較這兩個數據的訪問時效性,把訪問時間更久的數據淘汰出緩存。
為了避免操作鏈表的開銷,Redis 在實現 LRU 策略時使用了兩個近似方法:Redis 是用 RedisObject 結構來保存數據的,RedisObject 結構中設置了一個 lru 字段,用來記錄數據的訪問時間戳;Redis 並沒有為所有的數據維護一個全局的鏈表,而是通過隨機采樣方式,選取一定數量(例如 10 個)的數據放入候選集合,后續在候選集合中根據 lru 字段值的大小進行篩選。
Redis 在實現 LFU 策略的時候,只是把原來 24bit 大小的 lru 字段,又進一步拆分成了兩部分。
ldt 值:lru 字段的前 16bit,表示數據的訪問時間戳;
counter 值:lru 字段的后 8bit,表示數據的訪問次數。
當 LFU 策略篩選數據時,Redis 會在候選集合中,根據數據 lru 字段的后 8bit 選擇訪問次數最少的數據進行淘汰。當訪問次數相同時,再根據 lru 字段的前 16bit 值大小,選擇訪問時間最久遠的數據進行淘汰。
但是8bit最大能表示的值為255,假如A的訪問次數為255,而B的訪問次數為1024,所以也會被記為255,但是A的最近使用時間大於B的最近使用時間,所以當面臨淘汰A,B時會淘汰B
這樣顯然和我們預想的不同。
Redis 也注意到了這個問題。因此,在實現 LFU 策略時,Redis 並沒有采用數據每被訪問一次,就給對應的 counter 值加 1 的計數規則,而是采用了一個更優化的計數規則。
LFU 策略實現的計數規則是:每當數據被訪問一次時,首先,用計數器當前的值(即該數據當前被訪問的次數)乘以配置項 lfu_log_factor(該值控制計數器值增加的速度,值越大,增加的越慢) 再加 1,再取其倒數,得到一個 p 值;然后,把這個 p 值和一個取值范圍在(0,1)間的隨機數 r 值比大小,只有 p 值大於 r 值時,計數器才加 1。使用了這種計算規則后,我們可以通過設置不同的 lfu_log_factor 配置項,來控制計數器值增加的速度,避免 counter 值很快就到 255 了。
現在有一種特殊情況:
在一些場景下,有些數據在短時間內被大量訪問后就不會再被訪問了。那么再按照訪問次數來篩選的話,這些數據會被留存在緩存中,但不會提升緩存命中率。為此,Redis 在實現 LFU 策略時,還設計了一個 counter 值的衰減機制(較少counter值)。
LFU 策略使用衰減因子配置項 lfu_decay_time 來控制訪問次數的衰減。LFU 策略會計算當前時間和數據最近一次訪問時間的差值,並把這個差值換算成以分鍾為單位(精度為分鍾)。然后,LFU 策略再把這個差值除以 lfu_decay_time 值,所得的結果就是數據 counter 要衰減的值。
假設 lfu_decay_time 取值為 1,如果數據在 N 分鍾內沒有被訪問,那么它的訪問次數就要減 N。如果 lfu_decay_time 取值更大,那么相應的衰減值會變小,衰減效果也會減弱。所以,如果業務應用中有短時高頻訪問的數據的話,建議把 lfu_decay_time 值設置為 1,這樣一來,LFU 策略在它們不再被訪問后,會較快地衰減它們的訪問次數,盡早把它們從緩存中淘汰出去,避免緩存污染。
使用了 LFU 策略后,緩存還會被污染嗎?
我覺得還是有被污染的可能性,被污染的概率取決於LFU的配置,也就是lfu-log-factor和lfu-decay-time參數。
1、根據LRU counter計數規則可以得出,counter遞增的概率取決於2個因素:
a) counter值越大,遞增概率越低
b) lfu-log-factor設置越大,遞增概率越低
所以當訪問次數counter越來越大時,或者lfu-log-factor參數配置過大時,counter遞增的概率都會越來越低,這種情況下可能會導致一些key雖然訪問次數較高,但是counter值卻遞增困難,進而導致這些訪問頻次較高的key卻優先被淘汰掉了。
另外由於counter在遞增時,有隨機數比較的邏輯,這也會存在一定概率導致訪問頻次低的key的counter反而大於訪問頻次高的key的counter情況出現。
2、如果lfu-decay-time配置過大,則counter衰減會變慢,也會導致數據淘汰發生推遲的情況。
3、另外,由於LRU的ldt字段只采用了16位存儲,其精度是分鍾級別的,在counter衰減時可能會產生同一分鍾內,后訪問的key比先訪問的key的counter值優先衰減(原因可能是在同一分鍾內被訪問的數據,經過計算可能它們之間按照分鍾計算出來之后的精度可能差一分鍾),進而先被淘汰掉的情況。