Redis 內存淘汰策略,從根兒上理解

前言

本文參考源碼版本 redis6.2

Redis 基於內存設計,所有數據存放在內存,隨着時間推移,內存佔用也越來也高 ...

由於內存容量這個物理限制,我們需要在內存使用量達到一定比例後,做一些內存清理工作,以保證有足夠的空間來完成正常的處理。

在 Redis 中,完成這個工作的就是本文的主角 ------- Redis 內存淘汰機制。

如果讓你來設計一款內存淘汰策略,你會考慮哪些方面?

1)首先,從用戶體驗上看:

2)其次,從系統層面來看:

帶着這些問題,我們一起來探索下,Redis 是如何考慮並實現的。

一、淘汰策略

淘汰策略也是可以在 redis.config 中指定:

maxmemory-policy ...

截止目前,共有 8 種淘汰策略,如下:

1. 全局淘汰:

1)allkeys-lru:淘汰範圍是所有 keys,淘汰最久未使用的 key

2)allkeys-lfu:淘汰範圍是所有 keys,淘汰使用頻次最少的

3)allkeys-random:淘汰範圍:所有 keys,隨機淘汰 key

2. 淘汰 expire :

1)volatile-lru:淘汰範圍:所有設置了 expire 時間的 keys,淘汰最久未使用的 key

2)volatile-lfu:淘汰範圍:所有設置了 expire 時間的 keys,淘汰使用頻次最少的 key

3)volatile-random:淘汰範圍:所有設置了 expire 時間的 keys,隨機淘汰 key

4)volatile-ttl:淘汰範圍:所有設置了 expire 時間的 keys,淘汰 ttl 剩餘時間最少的 key

3. 不淘汰:

1)noeviction:不淘汰,意味着達到限制時,將無法存儲

二、淘汰算法

目前有兩種常用的內存淘汰算法,分別致力於從 訪問時間 和 訪問頻次 上解決內存問題。

1. LRU 算法

Least Recently Used,最近未使用淘汰算法,是一種按照訪問時間進行淘汰的算法。

假如只從設計上考慮,我們一般會定義一個隊列來存儲訪問記錄,然後每次從隊列末尾刪除元素即可。

但是在 redis 中,內存、CPU 是稀缺物,要儘可能減少內存使用量、CPU 的消耗,因此,在實現上也就更加放鬆。

redis 採用近似 LRU 算法,每次隨機選擇一個樣本集,並從樣本集中挑選最長時間未使用的 key 淘汰。這樣一來就不用維護訪問隊列,而是選擇一個合適的樣本集大小,也能達到近似 LRU 算法效果。

我們來看看官方測試效果:

在 redis3.0 之後,當選擇樣本集 maxmemory-samples = 10,其效果基本已經接近 LRU 算法,但是會更消耗 CPU。

redis.config 中的 maxmemory-samples 默認值爲 5,一般情況下應該夠用了。

接着,我們來看看 LRU 算法的工作原理,以及存在哪些問題:

~~~~~A~~~~~A~~~~~A~~~~A~~~~~A~~~~~A~~|
~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~|
~~~~~~~~~~C~~~~~~~~~C~~~~~~~~~C~~~~~~|
~~~~~D~~~~~~~~~~D~~~~~~~~~D~~~~~~~~~D|

其中,~ 字符表示 1 秒,| 表示結束,A 每 5 秒訪問一次,B 每兩秒訪問一次,C、D 每 10 秒訪問一次。

我們先看 A、B、C,由於 B 訪問頻次最高,你會看到 B 幾乎總是處於訪問隊列隊首,C 頻次最低,大概會出現在訪問隊列隊尾。

不同的是 D,和 C 一樣的頻率,目前卻出現在了隊首。

LRU 算法認爲,最近訪問過的 key,再次訪問的概率比較大,而很久沒有訪問的 key 再次訪問的概率比較小(隊尾),因此隊尾可以優先淘汰。

但是,從以上案例,D 的情況也可以看出,會存在一些極端情況。不過,LRU 總的來說,是比較 OK 的。

每一個 key 在 redis 中都是以 redisObject 形式存在,其中 lru 字段便是記錄訪問時間:

#define LRU_BITS 24

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
} robj;

2. LFU 算法

Least Frequently Used,即 最近最少使用淘汰策略,是一種按照訪問頻次處理的淘汰算法。相對於 LRU 算法,在某些方面效果可能更佳。

LRU 算法有個缺點是,臨時數據可能會取代真正經常使用的數據。比如,短時間內,大量臨時數據湧入 redis,而觸發發生內存淘汰,可能會將那些真正經常使用的數據驅逐。

LFU 是一種按頻次處理的淘汰算法,可以很好解決臨時數據問題。

假如只從設計上考慮 LFU,一般情況下:要記錄每個 key 的使用次數 + 統計窗口:

前面說了,redis 內存和 CPU 都是稀缺品,可以猜測下,不會採用上述方案。

redis LFU 也是採用給一個近似算法:

對應 redis.config 的配置是:

lfu-log-factor 10
lfu-decay-time 1

計算規則是這樣的:

從這個公式可以看出,影響累加器的有兩個變量:

我來來看看官方給出的參數 lfu_log_factor,控制累加速度效果:

+--------+------------+------------+------------+------------+------------+
| factor | 100 hits   | 1000 hits  | 100K hits  | 1M hits    | 10M hits   |
+--------+------------+------------+------------+------------+------------+
| 0      | 104        | 255        | 255        | 255        | 255        |
+--------+------------+------------+------------+------------+------------+
| 1      | 18         | 49         | 255        | 255        | 255        |
+--------+------------+------------+------------+------------+------------+
| 10     | 10         | 18         | 142        | 255        | 255        |
+--------+------------+------------+------------+------------+------------+
| 100    | 8          | 11         | 49         | 143        | 255        |
+--------+------------+------------+------------+------------+------------+

可以看到,隨着 lfu_log_factor 值的變大,計數器累加的速度越來越慢 ...

redis 爲了節省內存空間,LFU 的記錄仍然複用了 24 bit 的 LRU 字段:

           16 bits      8 bits
      +----------------+--------+
      + Last decr time | LOG_C  |
      +----------------+--------+

計數器累加相關邏輯:

  uint8_t LFULogIncr(uint8_t counter) {
      if (counter == 255) return 255;
      double r = (double)rand()/RAND_MAX;
      double baseval = counter - LFU_INIT_VAL;
      if (baseval < 0) baseval = 0;
      double p = 1.0/(baseval*server.lfu_log_factor+1);
      if (r < p) counter++;
      return counter;
  }

三、淘汰

前面我們已經討論瞭如何淘汰策略以及淘汰算法,這部分我們將討論真正的淘汰過程:

我畫了張淘汰的整體流程圖,你可以參考下:

1. 何時清理?

這也是一種惰性思想,一個用戶請求過來了,順便檢查下內存使用情況,當 redis 內存使用量達到預定閾值 maxmemory 時,便會觸發淘汰機制。

淘汰時也分兩種情況:

2. 清理哪些?

這個你基本也清楚了,哪些數據需要被清理,主要是看你選擇的淘汰策略,以及淘汰算法。

從淘汰策略來看:

從淘汰算法來看:

3. 清理多少?

待清理的大小 = used - maxmemory,即當前使用內存大小 - 設定的閾值。

一次性沒處理完(可以設置一定時長限制),可以扔到時間事件中,週期性的處理,直到達到目標 ...

4. 怎樣清理?

這就是兩種淘汰算法的工作了。LRU 和 LFU 在 redis 中都是近似算法,本質是爲了減少內存和 CPU 的消耗。

來源: https://juejin.cn/post/7142851076293656583

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/jFzAdV-FKl-HtYYAsMWg1g