Redis過期–淘汰機制的解析和記憶體佔用過高的解決方案

  • 2019 年 11 月 10 日
  • 筆記

echo編輯整理,歡迎轉載,轉載請聲明文章來源。歡迎添加echo微信(微訊號:t2421499075)交流學習。 百戰不敗,依不自稱常勝,百敗不頹,依能奮力前行。——這才是真正的堪稱強大!!!


Redis在我們平時的開發或者練習的時候,往往很容易忽略一個問題,那就是我們的Redis記憶體佔滿的問題。但是在真是的商業開發中,Redis的實際佔滿是真正會存在這樣的問題的。那麼如果Redis在某一刻佔滿記憶體,我們又沒有對它進行相應的設置它會出現什麼情況呢?會不會導致我們整個因為使用Redis而整個業務垮掉?這就是我們本篇文章所要講述的問題。

什麼是Redis淘汰機制

Redis記憶體淘汰機制其實簡單講就是將過期的數據或者很久沒有訪問,或者在一段時間內很少有訪問的數據進行刪除。它分為很多中,有主動的數據淘汰,如:用戶設定過期時間。有被動的淘汰,比如:Redis數據佔滿了記憶體,這個時候就會將過期的數據或者很久沒有訪問的數據刪除掉。

Redis的淘汰有哪些類型

  • 定時過期:每個設置過期時間的key都需要創建一個定時器,到過期時間就會立即清除。該策略可以立即清除過期的數據,對記憶體很友好;但是會佔用大量的CPU資源去處理過期的數據,從而影響快取的響應時間和吞吐量。
  • 惰性過期:只有當訪問一個key時,才會判斷該key是否已過期,過期則清除。該策略可以最大化地節省CPU資源,卻對記憶體非常不友好。極端情況可能出現大量的過期key沒有再次被訪問,從而不會被清除,佔用大量記憶體。
  • 定期過期:每隔一定的時間,會掃描一定數量的資料庫的expires字典中一定數量的key,並清除其中已過期的key。該策略是前兩者的一個折中方案。通過調整定時掃描的時間間隔和每次掃描的限定耗時,可以在不同情況下使得CPU和記憶體資源達到最優的平衡效果。
    (expires字典會保存所有設置了過期時間的key的過期時間數據,其中,key是指向鍵空間中的某個鍵的指針,value是該鍵的毫秒精度的UNIX時間戳表示的過期時間。鍵空間是指該Redis集群中保存的所有鍵。)

定時過期的問題:快取雪崩

很多人可能對這個詞很熟悉,因為有很多的商業案例展示了血淋淋的教訓,但是也有一部分人估計沒有接觸過。快取雪崩其實也不是什麼新詞,它主要的引起原因就是指快取中數據大批量的到過期時間。定時過期它本身就有一個缺點,那就是會佔用大量的CPU資源,如果我們主動設置過期時間的鍵過多,在同一時間過期,很有可能就會造就我們Redis掛掉。但是這並不是最可怕的,雪崩不僅僅影響自己,還在我們的業務中影響資料庫。因為我們很多業務設計都是在我們Redis的數據過期之後,從新查詢資料庫,但我們Redis主動批量過期的時候,會有大量的請求發送到我們的資料庫,很有可能導致我們的資料庫也掛掉。這才是最大的問題所在。

解決方案:

  • 快取數據的過期時間設置隨機,防止同一時間大量數據過期現象發生。
  • 如果快取資料庫是分散式部署,將熱點數據均勻分布在不同搞得快取資料庫中。
  • 設置熱點數據永遠不過期。

從幾種淘汰策略中其實我們可以看到基本的一些問題所在,所以我們在使用快取的時候最好有一個全面的了解和全面的考慮應對。在實際開發中,我們更應該多去關注的和了解的是定期過期,因為它涉及真實開發中的一些問題。所以我們應該提前設置好。

怎麼設置定期過期最大使用記憶體

定期過期的最大記憶體設置在我們的redis.conf文件中,我們可以在該文件中看到這個配置:maxmemory <bytes>,但是這個配置一般都是注釋掉的,也就是說安裝之後如果我們沒有主動對他進行配置,那麼他就不會有默認大小值。對應的它不設置的情況下,那麼它可以使用多少的記憶體空間呢?這個跟系統有關。如果說我們將Redis安裝在32位的系統上,它的最大使用記憶體空間應該是在3G左右,如果是64位的系統,那麼可以將我們的記憶體佔滿。當然如果真正佔滿記憶體,這是一件比較惡劣的事情,不僅僅訪問Redis的時候,我們不能在進行寫的操作,而且我們系統本身的其他操作也會受到限制。所以我們可以採用命令來對它進行一個初始化的設置

config set maxmemory 268435456

使用命令進行設置之後我們需要重啟Redis才能生效。當然我們也可以直接找到Redis的安裝目錄,然後使用vi命令,直接更改配置文件中的對應的該內容,更改完之後,重啟即可。

定期過期的淘汰策略

  • volatile-lru:根據LRU演算法生成的過期時間來刪除。
  • allkeys-lru:根據LRU演算法刪除任何key。
  • volatile-lfu:從所有配置了過期時間的鍵中驅逐使用頻率最少的鍵
  • allkeys-lfu:從所有鍵中驅逐使用頻率最少的鍵
  • volatile-random:根據過期設置來隨機刪除key。
  • allkeys-random:無差別隨機刪。
  • volatile-ttl:根據最近過期時間來刪除(輔以TTL)
  • noeviction:誰也不刪,直接在寫操作時返回錯誤。

隨機淘汰策略

隨機找hash桶再次hash指定位置的dictEntry即可。就是在場景REDIS_MAXMEMORY_VOLATILE_RANDOM和REDIS_MAXMEMORY_ALLKEYS_LRU情況下的待淘汰的key。我們可以一觀它的源碼:

dictEntry *dictGetRandomKey(dict *d)  {      dictEntry *he, *orighe;      unsigned int h;      int listlen, listele;        if (dictSize(d) == 0) return NULL;        if (dictIsRehashing(d)) _dictRehashStep(d);        if (dictIsRehashing(d)) {          // T = O(N)          do {              h = random() % (d->ht[0].size+d->ht[1].size);              he = (h >= d->ht[0].size) ? d->ht[1].table[h - d->ht[0].size] :                                        d->ht[0].table[h];          } while(he == NULL);      } else {          // T = O(N)          do {              h = random() & d->ht[0].sizemask;              he = d->ht[0].table[h];          } while(he == NULL);      }        /* Now we found a non empty bucket, but it is a linked       * list and we need to get a random element from the list.       * The only sane way to do so is counting the elements and       * select a random index. */      listlen = 0;      orighe = he;      while(he) {          he = he->next;          listlen++;      }      listele = random() % listlen;      he = orighe;      // T = O(1)      while(listele--) he = he->next;        return he;  }

TTL時間淘汰

for (k = 0; k < server.maxmemory_samples; k++) {      sds thiskey;      long thisval;        de = dictGetRandomKey(dict);      thiskey = dictGetKey(de);      thisval = (long) dictGetVal(de);        /* Expire sooner (minor expire unix timestamp) is better       * candidate for deletion */      if (bestkey == NULL || thisval < bestval) {          bestkey = thiskey;          bestval = thisval;      }  }

更多源碼,請參考redis官網。這裡只是簡單的展示兩種。我們可以根據這樣的程式碼來對我們的redis的定期過期做一個合理的配置

思考:

基於一個數據結構做快取,怎麼實現一個LRU演算法?

做一個有底線的部落格主