刪除大key時要小心

liuxuhui發表於2021-09-09

問題

redis大key是讓人比較頭疼的問題,如果線上redis出現大key,斷然不可立即執行del,因為大key的刪除會造成阻塞。阻塞期間,所有請求都可能造成超時,當超時越來越多,新的請求不斷進來,這樣會造成redis連線池耗盡,盡而引發線上各種依賴redis的業務出現異常。

做個簡單測試

透過指令碼先向redis寫入大量的資料:

127.0.0.1:6379> hlen hset_test
(integer) 3784945

這裡看到大概有300多萬的資料,我們執行個del看看:

127.0.0.1:6379> del hset_test
(integer) 1
(3.90s)

可以發現耗時將近4s
我們知道redis核心是單執行緒在跑的,那麼這個阻塞期間,redis是無法處理其他請求的。

低峰期刪除

最簡單的方式就是在業務低峰期進行刪除,比如大部分場景在凌晨4點左右比較低峰,這時候執行刪除,造成的影響比較小。當然這種方式也是無法避免阻塞期間的請求,一般適用執行期間qps非常小的業務。

scan分批

既然大key不能一下刪除,那麼我們就分批刪除。

hset

對於hset,我們hsan分批刪除。

# 虛擬碼
HSCAN key 0 COUNT 100
HDEL key fields

每次取個100條,然後刪除

set

對於set,我們可以每次隨機取一批資料,然後刪除

# 虛擬碼
SRANDMEMBER key 10
SREM key fields

zset

對於zset,每次可以直接刪除一批資料

虛擬碼
ZREMRANGEBYRANK key 0 10

list

對於list,直接pop

虛擬碼

i:=0

for {

    lpop key

    i++

    if i%100 == 0 {

        sleep(1ms)

    }

}


非同步刪除

過期key刪除策略

有人說既然線上刪除大key會造成阻塞,那麼就對這個key設定一個TTL,交給redis自己去刪。我先看看redis的過期key刪除策略:

定期刪除:

我們知道redis的key分為帶過期的和永久的,對於有過期時間的key,redis會單獨放在一個字典表裡,單獨的好處就是redis知道這個字典裡的key隨時可能過期,那麼我就定期過來處理下,定期的任務就交給了serveCron,預設每100ms執行一次。每當serveCron執行的時候,就會去帶ttl的key裡面隨機抽取一部分key來檢查,如果這批key真的過期了,那麼就執行同步刪除。隨機抽查的原因:

  1. 不可能全部檢驗的,阻塞執行緒

  2. 隨機的話體現一定的公平性

惰性刪除

透過定期刪除,我們可以每次刪除一批已經過期的key,但是如果一個key已經過期了,定期刪除也沒清理到,這時使用者來讀取這個key的話,肯定不能直接返回,這時也會檢查這個key是否過期,如果過期直接刪除,返回空。

淘汰策略

  1. noeviction:當記憶體使用超過配置的時候會返回錯誤,不會驅逐任何鍵

  2. allkeys-lru:透過LRU演算法驅逐最久沒有使用的鍵

  3. volatile-lru:透過LRU演算法從設定了過期時間的鍵集合中驅逐最久沒有使用的鍵

  4. allkeys-random:從所有key中隨機刪除

  5. volatile-random:從過期鍵的集合中隨機驅逐

  6. volatile-ttl:從配置了過期時間的鍵中驅逐馬上就要過期的鍵

  7. volatile-lfu:從所有配置了過期時間的鍵中驅逐使用頻率最少的鍵

  8. allkeys-lfu:從所有鍵中驅逐使用頻率最少的鍵

淘汰策略是一個靈活的選項,一般根據業務來選擇合適的淘汰策略,那麼自定義的淘汰策略是何時觸發的?當然是我們進行加key或者更新一個更大的key的時候。所以他的刪除也是同步的,如果正好淘汰一個大key的時候,很不幸當前也會發生阻塞。

總結:不管以上三種哪個觸發的刪除,它都是同步的。所以就算加個TTL,redis也是同步刪除的,大key還是會造成阻塞。

非同步刪除

在redis4.0的時候,作者對於大key刪除造成阻塞的問題也做了考慮,於是出現了非同步刪除,非同步刪除也分為使用者主動和程式被動。

主動刪除

unlink

對於主動刪除,redis提供了del的替代方法unlink,當我們在unlink的時候,redis會先檢查要刪除元素的個數(比如集合),如果集合的元素的小於等於64個的時候,就會直接執行同步刪除,因為這不算一個大key,不會浪費很多的開銷,但是當超過64個的時候,redis會認為是大key的機率比較大,這時候redis會在字典裡,先把key刪除,真正的value會交給非同步執行緒來操作,這樣的話就不會對主執行緒造成任何影響。

flushall、flushdb

在執行flushall或者flushdb的時候,增加了ASYNC選項 FLUSHALL [ASYNC] ,當使用者沒設定ASYNC的時候,此時的flush操作是阻塞的,當設定了ASYNC的時候,會建立一個新的空字典,然後指向它,老字典交給非同步執行緒來慢慢刪。

被動刪除

redis配置策略

  • lazyfree-lazy-eviction:針對redis有設定記憶體達到maxmemory的淘汰策略時,這時候會啟動非同步刪除,此場景非同步刪除的缺點就是如果刪除不及時,記憶體不能得到及時釋放。

  • lazyfree-lazy-expire:對於有ttl的key,在被redis清理的時候,不執行同步刪除,加入非同步執行緒來刪除。

  • replica-lazy-flush:在slave節點加入進來的時候,會執行flush清空自己的資料,如果flush耗時較久,那麼複製緩衝區堆積的資料就越多,後面slave同步資料較相對慢,開啟replica-lazy-flush後,slave的flush可以交由非同步現成來處理,從而提高同步的速度。

  • lazyfree-lazy-server-del:這個選項是針對一些指令,比如rename一個欄位的時候 RENAME key newkey, 如果這時newkey是存在的,對於rename來說它就要刪除這個newkey的value,如果這個newkey是一個大key,那麼就會造成阻塞,當開啟了這個選項時也會交給非同步執行緒來操作,這樣就不會阻塞主執行緒了。

題外話:rename

先來做個測試:

127.0.0.1:6379> set A 1
OK

127.0.0.1:6379> eval "for i=1,10000000,1 do redis.call('hset','B', i,1) end" 0
(15.89s)
  1. 設定A為1

  2. 向B裡面新增1000w的資料

B肯定是大key了,這時想把A重新命名成B執行rename A B

127.0.0.1:6379> rename A B
OK
(11.07s)

發現阻塞了,這是因為redis刪除B造成的,如果有rename的場景一定要注意newkey是否已經存在,newkey是否是大key。


作者:假裝懂程式設計
連結:
來源:掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/756/viewspace-2797389/,如需轉載,請註明出處,否則將追究法律責任。

相關文章