Redis 對過時數據的處理

Redis 對過時數據的處理redis

在 redis 中,對於已通過期的數據,Redis 採用兩種策略來處理這些數據,分別是惰性刪除和按期刪除算法

惰性刪除服務器

惰性刪除不會去主動刪除數據,而是在訪問數據的時候,再檢查當前鍵值是否過時,若是過時則執行刪除並返回 null 給客戶端,若是沒有過時則返回正常信息給客戶端。微信

它的優勢是簡單,不須要對過時的數據作額外的處理,只有在每次訪問的時候纔會檢查鍵值是否過時,缺點是刪除過時鍵不及時,形成了必定的空間浪費。數據結構

源碼dom

robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) {
    robj *val;

    if (expireIfNeeded(db,key) == 1) {
        /* Key expired. If we are in the context of a master, expireIfNeeded()
         * returns 0 only when the key does not exist at all, so it's safe
         * to return NULL ASAP. */
        if (server.masterhost == NULL) {
            server.stat_keyspace_misses++;
            notifyKeyspaceEvent(NOTIFY_KEY_MISS, "keymiss", key, db->id);
            return NULL;
        }

        /* However if we are in the context of a slave, expireIfNeeded() will
         * not really try to expire the key, it only returns information
         * about the "logical" status of the key: key expiring is up to the
         * master in order to have a consistent view of master's data set.
         *
         * However, if the command caller is not the master, and as additional
         * safety measure, the command invoked is a read-only command, we can
         * safely return NULL here, and provide a more consistent behavior
         * to clients accessign expired values in a read-only fashion, that
         * will say the key as non existing.
         *
         * Notably this covers GETs when slaves are used to scale reads. */
        if (server.current_client &&
            server.current_client != server.master &&
            server.current_client->cmd &&
            server.current_client->cmd->flags & CMD_READONLY)
        {
            server.stat_keyspace_misses++;
            notifyKeyspaceEvent(NOTIFY_KEY_MISS, "keymiss", key, db->id);
            return NULL;
        }
    }
    val = lookupKey(db,key,flags);
    if (val == NULL) {
        server.stat_keyspace_misses++;
        notifyKeyspaceEvent(NOTIFY_KEY_MISS, "keymiss", key, db->id);
    }
    else
        server.stat_keyspace_hits++;
    return val;
}

按期刪除ide

按期刪除:Redis會週期性的隨機測試一批設置了過時時間的key並進行處理。測試到的已過時的key將被刪除。測試

具體的算法以下:ui

  • Redis配置項hz定義了serverCron任務的執行週期,默認爲10,表明了每秒執行10次;this

  • 每次過時key清理的時間不超過CPU時間的25%,好比hz默認爲10,則一次清理時間最大爲25ms;

  • 清理時依次遍歷全部的db;

  • 從db中隨機取20個key,判斷是否過時,若過時,則逐出;

  • 如有5個以上key過時,則重複步驟4,不然遍歷下一個db;

  • 在清理過程當中,若達到了25%CPU時間,退出清理過程;

雖然redis的確是不斷的刪除一些過時數據,可是不少沒有設置過時時間的數據也會愈來愈多,那麼redis內存不夠用的時候是怎麼處理的呢?這裏咱們就會談到淘汰策略

Redis內存淘汰策略

當redis的內存超過最大容許的內存以後,Redis會觸發內存淘汰策略,刪除一些不經常使用的數據,以保證redis服務器的正常運行

在redis 4.0之前,redis的內存淘汰策略有如下6種

  • noeviction:當內存使用超過配置的時候會返回錯誤,不會驅逐任何鍵
  • allkeys-lru:加入鍵的時候,若是過限,首先經過LRU算法驅逐最久沒有使用的鍵
  • volatile-lru:加入鍵的時候若是過限,首先從設置了過時時間的鍵集合中驅逐最久沒有使用的鍵
  • allkeys-random:加入鍵的時候若是過限,從全部key隨機刪除
  • volatile-random:加入鍵的時候若是過限,從過時鍵的集合中隨機驅逐
  • volatile-ttl:從配置了過時時間的鍵中驅逐立刻就要過時的鍵
    在redis 4.0之後,又增長了如下兩種
  • volatile-lfu:從全部配置了過時時間的鍵中驅逐使用頻率最少的鍵
  • allkeys-lfu:從全部鍵中驅逐使用頻率最少的鍵

內存淘汰策略能夠經過配置文件來修改,redis.conf對應的配置項是maxmemory-policy 修改對應的值就行,默認是noeviction

LRU(the least recently used 最近最少使用)算法

若是一個數據在最近沒有被訪問到,那麼在將來被訪問的可能性也很小,所以當空間滿的時候,最久沒有被訪問的數據最早被置換(淘汰)

LRU算法一般經過雙向鏈表來實現,添加元素的時候,直接插入表頭,訪問元素的時候,先判斷元素是否在鏈表中存在,若是存在就把該元素移動至表頭,因此鏈表的元素排列順序就是元素最近被訪問的順序,當內存達到設置閾值時,LRU隊尾的元素因爲被訪問的時間線較遠,會優先踢出
file

file

可是在redis中,並無嚴格實行LRU算法,之因此這樣是由於LRU須要消耗大量的額外內存,須要對現有的數據結構進行較大的改造,近似LRU算法採用在現有數據結構的基礎上使用隨機採樣法來淘汰元素,能達到和LRU算法很是近似的效果。Redis的 LRU算法給每一個key增長了一個額外的長度爲24bit的小字段,記錄最後一次被訪問的時間戳。

redis經過maxmemory-samples 5配置,對key進行採樣淘汰。同時在Redis3.0之後添加了淘汰池進一步提高了淘汰準確度。

可是LRU算法是存在必定的問題

例如,這表示隨着時間的推移,四個不一樣的鍵訪問。每一個「〜」字符爲一秒鐘,而「 |」 最後一行是當前時刻。

~~ B ~~ B ~~ B ~~ B ~~ B ~~ B ~~ B ~~ B ~~ B ~~ B ~~ B ~~ B〜|

~~~~~~~~~~ C ~~~~~~~~ C ~~~~~~~~~ C ~~~~~~ |

~~~~~ D ~~~~~~~~~ D ~~~~~~~ D ~~~~~~~~ D |


在上圖中,按照LRU機制刪除的話刪除的順序應該是C->A->B->D 其實這並非咱們想要的,由於B被訪問的頻率是最高的,而D被訪問的頻率比較低,因此咱們更想讓B保留,把D刪除,因此咱們接下來看另外一種策略  LFU

**LFU(leastFrequently used 最不常用)**

若是一個數據在最近一段時間內不多被訪問到,那麼能夠認爲在未來他被訪問到的機率也很小。因此,當空間滿時,最小頻率訪問的數據最早被淘汰

Redis使用redisObject中的24bit  lru字段來存儲lfu字段, 這24bit被分爲兩部分:

1:高16位用來記錄訪問時間(單位爲分鐘) 

2:低8位用來記錄訪問頻率,簡稱counter

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

可是counter 8bit很容易就溢出了,技巧是用一個邏輯計數器,給予機率的對數計數器,而不是一個普通的遞增計數器
```
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.0/((counter - LFU_INIT_VAL)*server.lfu_log_factor+1);
```
其中LFU_INIT_VAL爲5,其實簡單說就是,越大的數,遞增的機率越低
嚴格按照LFU算法,時間越久的key,counter越有可能越大,被剔除的可能性就越小。counter只增加不衰減就沒法區分熱點key。爲了解決這個問題,redis提供了衰減因子server.lfu_decay_time,其單位爲分鐘,計算方法也很簡單,若是一個key長時間沒有訪問那麼他的計數器counter就要減小,減小的值由衰減因子來控制

> 關注個人技術公衆號,每週都有優質技術文章推送。
微信掃一掃下方二維碼便可關注:
![file](https://img2020.cnblogs.com/other/1094508/202010/1094508-20201018224534910-39047834.png)
相關文章
相關標籤/搜索