Redis快取刪除驅逐策略的工作方式 - codemancers

banq發表於2021-05-26

Redis是最流行的應用程式快取儲存形式之一。Redis還可以用作具有正確配置型別的永續性資料儲存。在此部落格中,我們介紹了這些配置在現實情況下如何發揮作用。我們還討論了在將Redis用於我們的應用程式時如果不仔細考慮此類配置會發生什麼情況。
首先,讓我們為實驗設定環境。為此,我們將使用docker執行一個簡單的Redis伺服器。除此之外,您還需要編寫一些有用的指令碼來輕鬆檢查我們的快取儲存區的狀態。我將在這裡使用Ruby,但是Python也能正常工作!
讓我們在docker上啟動一個redis容器。

$ docker run -d -p 6379:6379 redis:latest



Redis的maxmemory配置可以定義允許從主機系統使用多少記憶體。讓我們使用redis-cli檢查當前配置是什麼。預設情況下,redis將maxmemory的值保持為0,並且不會管理或限制記憶體的使用。但是,在32位系統的情況下,它將仍然受到作業系統的可用RAM或3GB的限制。此外,redis透過maxmemory-policy用於刪除資料以管理記憶體使用情況。讓我們使用redis-cli檢查當前配置是什麼:

127.0.0.1:6379> CONFIG GET maxmemory-policy
1) "maxmemory-policy"
2) "noeviction"

預設情況下,redis將maxmemory-policy的值保持為noeviction,並且永遠不會自行刪除任何資料。這也是一種有意識的配置,可讓人們決定如何使用Redis。如上所述,如果某人希望其Redis保留資料,則無需啟用逐出功能。
 

那麼怎麼可能出問題呢?
對於在高併發使用者基礎生產環境上執行的應用程式,如果不注意此配置,可能會導致Redis停機。
為啥?正如我們已經要求redis伺服器不要限制其記憶體使用,並且不刪除快取策略意味著redis中的資料將繼續增長,在某個時候,它達到了臨界水平。Redis不能再向其中新增新記錄,因為它受到可用記憶體的限制。
系統由於記憶體不足而窒息而殺死了Redis,並且由於Redis拒絕連線,我們的應用開始崩潰。
由於redis是記憶體解決方案,因此我們必須增加系統的RAM,然後重新啟動它以使其再次執行。即使從技術上講增加RAM是可行的解決方案,但在財務上這通常不是可行的解決方案。
  

Redis作為快取儲存
如果有人希望將Redis用作快取儲存該怎麼辦?首先,我們應該注意什麼?
顧名思義,快取記憶體儲存是一種針對短期資料的解決方案,可以在適當的時間清除這些資料。快取記憶體用於需要定期高速訪問某些資料集的應用程式中。當應用程式希望將正在進行的任務的中間後設資料儲存幾個小時甚至幾天時,也會使用快取。這表明我們可以要求Redis從其記憶體中刪除不需要的或過時的記錄。有兩個好處:

  • 更少的RAM需求=>更少的成本障礙
  • 停機時間幾乎為零=>現在誰會不喜歡呢?

 

我們應該如何配置它?
為了更好地瞭解應該如何配置Redis,我們需要檢視Redis提供給我們的選項。官方文件明確說明了我們可以使用的配置。簡而言之,我們需要更改maxmemory和maxmemory-policy值,以完全控制redis伺服器來管理其記憶體份額。
讓我們做一些有趣的實驗,看看這些配置如何產生作用。我們將看看三種常見的驅逐政策allkeys-lru,volatile-lru以及volatile-ttl。
首先,我們需要設定兩個配置。

127.0.0.1:6379> CONFIG SET maxmemory 1mb
OK


為了模擬低記憶體的情況,我們將其允許的記憶體限制為1mb。此時,Redis可能儲存的記錄數量非常有限。記憶體已滿後,redis kick將啟動其機率驅逐演算法,以確定可以刪除哪些鍵以為新記錄騰出空間。作為使用者,我們可以控制如何選擇這些鍵來逐出刪除。
 

案例:allkeys-lru,只給我該死的空間!
沒錯,allkeys-lru無需任何特殊考慮即可從記憶體中刪除最近最少使用的key:

127.0.0.1:6379> CONFIG SET maxmemory-policy allkeys-lru
OK

切換到ruby控制檯以執行一些指令碼來讀取和寫入資料,以檢視哪些鍵被刪除。我們可以使用redis-rb gem連線到redis。

irb> $redis = Redis.new(url: "redis://localhost:6379")
irb> $redis.set("constant-1", "foo")
# "OK"
irb> $redis.set("constant-2", "bar")
# "OK"
irb> $redis.set("constant-3", "tar")
# "OK"
irb> $redis.get("constant-2")
# "bar"
irb> $redis.get("constant-1")
# "foo"

# add many records
irb> (1..500).each { |key| $redis.set("loop-#{key}", SecureRandom.uuid)}

# get some of the keys to mimic usage
irb> (3..100).each do |key|
        unless $redis.get("loop-#{key}")
          puts "loop-#{key} evicted"
        else
          puts "loop-#{key} found"
        end
      end
# loop-3 found
# .
# .
# loop-28 found
# .
# .
# loop-100 found

# now let's flood redis with more data
irb> (501..1500).each { |key| $redis.set("loop-#{key}", SecureRandom.uuid)}
irb> $redis.get("constant-3")
# nil
irb> $redis.get("loop-1")
# nil
irb> $redis.get("loop-12")
# "ea6ef190-05d4-480c-8e47-6785d80ca4d1"
irb> $redis.get("loop-100")
# "fd8ff9e1-6ee2-4e74-aaff-1043b7c24e67"
irb> $redis.get("loop-242")
# nil
irb> $redis.get("loop-1499")
# "88f17049-0e11-4cbc-8485-e000df21a2c3"

  • 我們最初新增了三個鍵,constant-1,2,3並訪問了其中兩個。
  • 我們在一個迴圈中新增了500條記錄,這消耗了大量的記憶體。
  • 我們訪問了那些鍵的子集來模仿最近使用的鍵。IE。我們會使用3到100之間的鍵。redis中所有鍵仍然可用。
  • 我們再次開始用1000條記錄充斥Redis,Redis達到了記憶體限制。
  • Redis根據LRU策略選擇鍵並將其刪除。
  • 我們隨機檢查一些鍵以確認我們在步驟3中查詢的鍵在redis中仍然可用,但是步驟2和4中的大多數鍵已被清除。
  • 我們還可以注意到,最近插入的鍵,例如。loop-1499仍然保留

在這種情況下,很明顯,redis只關心最近使用過的鍵,並刪除所有其他鍵,而無需任何進一步的考慮。

127.0.0.1:6379> FLUSHDB
OK

 

案例:volatile-lru,到期或被刪除!
與allkeys-lru不同,volatile-lru需要特別注意從記憶體中刪除最近最少使用的鍵:鍵必須設定有效期限。

127.0.0.1:6379> CONFIG SET maxmemory-policy volatile-lru
OK

讓我們再次切換到ruby控制檯並測試這種情況。與最後一種情況的唯一區別是,當我們新增新資料時,我們還將為每條記錄設定一個到期時間。

# set some records without any expiry first
irb> $redis.set("constant-1", "foo")
# "OK"
irb> $redis.set("constant-2", "bar")
# "OK"
irb> $redis.set("constant-3", "tar")
# "OK"
irb> $redis.get("constant-1")
# "foo"

# add many records
# instead of using multi, we can also pass expiry as $redis.set(key, val, ex: 2345)
# but this is not supported by HSET
irb> (1..500).each do |key|
        $redis.multi do |multi|
          multi.set("loop-#{key}", SecureRandom.uuid)}
          multi.expire("loop-#{key}", key*20)
        end
      end

# get some of the keys to mimic usage
irb> (3..100).each do |key|
        unless $redis.get("loop-#{key}")
          puts "loop-#{key} evicted"
        else
          puts "loop-#{key} found"
        end
      end
# loop-3 found
# .
# .
# loop-38 found
# .
# .
# loop-100 found

# now let's flood redis with more data
irb> (501..1500).each do |key|
        $redis.multi do |multi|
          multi.set("loop-#{key}", SecureRandom.uuid)}
          multi.expire("loop-#{key}", key*20)
        end
      end
irb> $redis.get("constant-1")
# "foo"
irb> $redis.get("constant-2")
# "bar"
irb> $redis.get("constant-3")
# "tar"
irb> $redis.get("loop-1")
# nil
irb> $redis.get("loop-100")
# "ef32bffc-6b24-4f0d-b2ba-01ef72e94537"
irb> $redis.get("loop-101")
# nil
irb> $redis.get("loop-1499")
# "55f1c049-3e14-4cbc-8485-e000df21a2c3"
# .
# .
# after 1499*20(=29980) seconds
irb> $redis.get("loop-1499")
# nil

  1. 我們最初新增了三個鍵,constant-1,2,3並且只訪問了其中一個。
  2. 我們在一個迴圈中新增了500條到期的記錄,這消耗了大量的記憶體。
  3. 我們訪問了那些鍵的子集來模仿最近使用的鍵。IE。我們訪問的鍵範圍是3到100。
  4. 我們再次開始用1000條記錄充斥Redis,Redis達到了記憶體限制。
  5. Redis會根據LRU策略選擇到期的鍵並將其刪除。
  6. 我們隨機檢查一些鍵以確認我們在步驟3中查詢的鍵仍可在Redis中使用,來自步驟2的所有鍵以及來自步驟4的大多數鍵由於它們已過期或被強行刪除而已被撤出。
  7. 我們還可以注意到在步驟1中插入的​​鍵,例如。常數1,2,3仍然保留。這些鍵沒有到期時間,因此演算法將忽略它們!

因此,在這種情況下,情況略有偏離,但是我們仍在基於LRU技術刪除鍵。讓我們看一下第三種情況如何為它增加一個條件。

 

案例:volatile-ttl,最大限度地減少生存!
在上述情況下,我們看到了redis如何刪除過期的鍵,volatile-ttl是幾乎相同,只不過還會刪除即將到期的鍵,而不是選擇最近最少使用的記錄。另外,請注意,在這種情況下,redis也會忽略沒有為記錄設定任何過期時間的鍵。
 

何時/哪個使用?
這個問題的答案歸結為我們正在努力實現的目標。

  1. 您是否有一項任務需要X秒鐘的記錄,而另一些在生存時間長短方面不那麼重要?->使用具有可變TTL的volatile-ttl進行記錄
  2. 您是否有某些鍵需要始終在Redis中可用,但還需要進行常規清理?->選擇volatile-lru
  3. 您是否只是在很短的時間內將資料轉儲到Redis中,然後再不再使用它?還是不確定?->堅持使用更安全的allkeys-lru



 

相關文章