redis淘汰+過期雙向保證高可用 | redis 為什麼那麼快?

煙花散盡13141發表於2021-06-28

前言

redis和資料相比除了他們的結構型顛覆以外!還有他們儲存位置也是不相同。傳統資料庫將資料儲存在硬碟上每次資料操作都需要IORedis是將資料儲存在記憶體上的。這裡稍微解釋下IO是啥意思。IO就是輸入流輸出流方式將資料在硬碟和記憶體之間進行互動!而redis直接在記憶體上就剩下了IO操作。這也是redis快的原因之一吧

  • 記憶體相對於硬碟來說很寶貴。我們平時的電腦也是硬碟是記憶體的幾百倍。既然記憶體很寶貴而redis又將資料儲存在記憶體上那麼redis肯定不能肆無忌憚的進行儲存 。這就需要redis和開發者們作出相應的優化
  • 首先redis在配置檔案(redis.conf)中通過maxmemory引數指定redis 設定整個對記憶體的支配大小!
  • 其次就是要求我們開發者在想redis中填值的時候根據自己的需求設定相應的key過期時間。這樣不必要的資料就會被redis過期驅逐策略清楚。從而節省記憶體供別人使用

本文凌駕於redis基礎之上,這裡筆者預設大家都已經安裝了redis . 並實際使用過redis

記憶體分配

  • maxmemory 指定大小。在redis.conf配置檔案中可以直接指定。他的單位時byte。

image-20210617144838455

  • 上圖中註釋部分是給大家的解釋,實際中#配置需要換行哈!!!友情提示
  • 另外我們可以連線上redis通過config命令來設定

image-20210617145851812

  • 兩種方式都可以設定,前者是全域性設定重啟之後仍然有效!後者是臨時設定重啟之後就會重新載入redis.conf中的配置。

鍵過期

  • 上面我們從redis本身角度出發設定了記憶體限制,這樣不用擔心他們吞噬系統記憶體!下面就需要我們開發者設計角度約束自己了。

設定過期時間

expire key time
  • 設定過期時間預設單位時S 。

  • 然後通過ttl 命令可以檢視剩餘過期時間

image-20210617151445320

  • 經過多次執行ttl能夠觀察到剩餘時間在不斷的減少!當減少到0的時候就被給驅逐策略驅逐!注意這裡說的是驅逐策略驅逐並不是意味著立馬被刪除

更新過期時間

  • del key 直接將key刪除了那麼該key對應的過期自然也就不存在了!這種情況筆者也算作是更新過期時間
  • set getset等命令重新設定key、value方式會覆蓋過期時間 , 直接被覆蓋成-1

image-20210617152121178

  • setgetset包括del嚴格意義是覆蓋過期時間。真正做到更新過期時間的還是expire .在expire是已最新為準的!
  • 上面其實都修改了key才會應發原本的過期時間失效的!因為此key非彼key 。 但是appendincr 等命令是改變值這種命令是不會影響到原來的過期設定的

image-20210617152731013

淘汰策略

  • 根據上面配置我們可以將我們的redis最大記憶體設定為1MB , 設定大小隨便最好能小點。然後我們通過Java小程式不斷向redis中填充。最終當記憶體不夠使用時就會報錯

image-20210617161613440

  • 報錯就是因為記憶體滿了,新增的key被redis拒絕了!不僅僅是新增的被拒絕,就算此時我們想改變已經在redis中的key的值也是不可用的
public static void main(String[] args) {
    Jedis jedis = new Jedis("39.102.60.114", 6379);
    jedis.auth("Qq025025");
    Integer index = 1;
    while (true) {
        String uuid = UUID.randomUUID().toString();
        jedis.set(index.toString(), uuid);
        System.out.println(index++);
    }
}

image-20210617162804867

  • 不管是append 還是set 都是報OOM command not allowed when used memory > maxmemory 。程式碼中列印和redis鍵個數一致;說明我們預設的淘汰策略是直接拒絕

  • 總結下來就是:當redis記憶體被使用滿了後,任何的寫操作都會被拒絕!

  • 當沒有足夠記憶體時難道就這麼直接拒絕嗎?上面也提到了需要我們程式設計師自己根據需求設定鍵過期已釋放記憶體供其他有需要的key使用!那麼設定了過期key之後這些key又是怎麼被清除的呢? 這就牽扯出我們的淘汰策略

image-20210617163430236

volatile-lru【最近很少使用】

當記憶體告警時redis會將近期很少使用且設定了過期時間的key剔除出去,即使該key還沒有到過期時間。如果沒有符合的key也就是執行之後記憶體仍然不足時將會和預設淘汰策略noeviction丟擲一樣的錯誤OOM command not allowed when used memory > maxmemory

  • 首先我們在redis.conf中配置我們最近很少使用策略. maxmemory-policy volatile-lru 。 然後重啟我們的redis服務 。重啟之後flushall清空所有資料,我們在通過上面的Java程式重新生成下資料!

image-20210617165407096

  • Java程式中我們設定前100個key新增過期時間
public static void main(String[] args) {
    Jedis jedis = new Jedis("39.102.60.114", 6379);
    jedis.auth("Qq025025");
    Integer index = 1;
    while (true) {
        String uuid = UUID.randomUUID().toString();
        if (index < 100) {
            jedis.setex(index.toString(),360, uuid);
        } else {
            jedis.set(index.toString(), uuid);
        }
        System.out.println(index++);
    }
}

image-20210617171334220

  • 簡單分析下為什麼程式計數器大於redis庫中的key數量!就是因為我們為前100設定了過期時間。當記憶體不足時redis就會將當前設定了過期時間的key中最近最少使用的key進行剔除!所以我們計數器會大於鍵數量。因為有部分鍵被清除了!我們獲取前100的key都是null , 說明被刪除了! 那麼為什麼本次計數器不是比上次多100 。 那是因為我們每次儲存進來的是uuid, 所佔長度都不是固定的。還有本身淘汰策略也是佔用記憶體的

image-20210617173746769

策略總結

  • 上面演示了最近最少使用的淘汰策略!除此之外還有其他的策略
noeviction:拒絕寫請求,正常提供讀請求,這樣可以保證已有資料不會丟失(預設策略);
2. volatile-lru:嘗試淘汰設定了過期時間的key,雖少使用的key被淘汰,沒有設定過期時間的key不會淘汰;
3. volatile-ttl:跟volatile-lru幾乎一樣,但是他是使用的key的ttl值進行比較,最先淘汰ttl最小的key;
4. volatile-random:其他同上,唯一就是他通過很隨意的方式隨機選擇淘汰key集合中的key;
5. allkeys-lru:區別於volatile-lru的地方就是淘汰目標是全部key,沒設定過期時間的key也不能倖免;
6. allkeys-random:這種方式同上,隨機的淘汰所有的key。
  • 使用哪種淘汰策略需要我們結合自己的專案場景來配合使用!!!

過期刪除

  • 上面我們從【鍵過期】、【淘汰策略】兩個角度分析了redis 。 僅僅這兩方面還沒有完全高效使用記憶體!淘汰策略是瀕臨記憶體不足時觸發。那麼當設定了過期時間的鍵真正到了過期時間而此時記憶體尚夠使用?這種場景是不是需要將過期鍵刪除呢?
  • 因為redis是單執行緒,那麼在鍵過期的時候如何不影響對外服務的同時清除過期鍵呢?答案是【不行】。嚴格意義是無法解決的因為單執行緒同時只能做一件事!既然無法解決那麼我們可以達到一種協調狀態!如果同一時刻出現一個過期鍵那麼清除鍵很快這時候阻塞外部服務的時間很短可能毫秒級設定納秒級!
  • 但是如何同一時間發生上萬鍵過期,如果想要刪除上萬鍵那肯定需要花費一定時間這時候就會阻塞對外服務!這肯定是不能接受的,阻塞時間過長會導致客戶端連線超時報錯的。這在併發場景下更是無法接受的!所以redis如何應對同一時間過多資料過期的場景,他的刪除過期鍵的方法略有不同!

定時清除

  • 針對每個過期鍵設定一個定時器,在過期時就會進行清理該鍵!
  • 該做法能夠做到資料實時被清理從而保證記憶體不會被長期佔用!提高了記憶體的使用率!
  • 但是問題也隨之而來,每一個key需要設定一個定時器進行跟蹤。redis這裡筆者猜測應該是啟用另外執行緒來進行定時跟蹤!這裡有清除的還請幫忙解答下?
  • 當同一時間過期key很多的時候!我們的CPU就需要不斷的執行這些定時器從而導致CPU資源緊張。最終會影響到redis服務的效能

定期清除

image-20210618101552195

  • 定期刪除就是上面我們圖示效果,redis會每隔100ms執行一次定時器,定時器的任務就是隨機抽取20個設定過期的key 。 判斷是否進行清除。上面圖示中說明中寫錯了不是10S , 而是每隔100ms 。請原諒我的粗心!!!

image-20210618102943955

  • 定期刪除和定時刪除作用是相反的!定期刪除是將key集中進行處理同時為了保證服務的高可用在處理時加入的時間限制。每次執行總時長不能超過25ms 。 也就是說對於客戶端來說服務端的延遲不會超過25ms
  • 他的優點就是不需要CPU頻繁的進行操作key清除!因為他是定期進行清除所以就會導致一部分資料沒有來得及清除從而導致記憶體使用上會被一直佔用!

惰性清除

  • 關於惰性刪除我們在平時開發中也經常使用這種方式!當資料過期時redis並不急著去清除這些資料,而是等到該key被再次請求時進行刪除!這樣在最終效果上是沒有問題的。
  • 優點和定期清除一樣他保證了CPU不必頻繁的進行切換!但是缺點也很明顯會導致很多已經過期的key任然在redis中。

惰性清除+定期清除

  • 我們開頭說過了既要高可用又要實時清理過期key 這是無法做到的!既然無法做到我們就需要在CPU和記憶體中間做一個權衡!redis內部是使用惰性清除和定期清除兩種方式結合使用,最終保重CPU和記憶體之間的一種平衡!

總結

  • 相信大家對上面三個概念有點模糊了。【鍵過期】、【淘汰策略】、【過期清除】
  • 首先【鍵過期】是redis給我們開發者提供的功能。我們可以根據自己的業務需求合理的設定鍵的過期時間,從而保證記憶體的高可用
  • 其次【過期清除】在我們之前設定的過期的key如何進行合理的清除,並不能一股腦一下子進行清除因為資料過大會導致服務的卡頓。這個時候我們需要通過定期清除減緩清除key程式碼的卡頓。在redis.conf中我們可以設定 hz 10 代表1S中平均執行幾次這也是我們上面所說的100MS的由來。但是僅僅定期刪除會產生遺漏資料所以我們還需要加上惰性清除,最終保證對客戶端來說資料是準確實時清除的。
  • 那麼關於【淘汰策略】又是啥呢?在上面過期清除是如果使用者一直不請求過期的key ,並且隨著業務產生越來越多的過期key . 這時候redis服務中還會堆積很多過期的無效key 。這個時候如果記憶體不夠用了的話那又該怎麼辦呢?這時候我們需要設定淘汰策略比如果volatile-lru . 就會將最近最少使用的設定過期key進行清除從而保證儘可能的接收更多的有效資料!
  • 這就是為什麼會設計三者的原因!好好理解上面三個主題我們再去想想為什麼會發生【快取雪崩】、【快取奔潰】、【快取擊穿】就好理解一點了呢?
  • 關於上述的redis常見的災難場景,我們下章節繼續分析如何產生的、並且如何進行修復進行展開討論。

相關文章