iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0

Day 4 的商品快取每個 key 都設了 TTL,理由是「不設就永遠佔著記憶體」。

但設了 TTL 的 key,時間到的那一秒記憶體就還回來了嗎?並沒有。key 過期一分鐘之後,DBSIZE 還算得到它們,記憶體也還佔著。

常用指令

今天沒有新指令,是拿幾個看得到狀態的指令來觀察:

  • TTL key(剩幾秒,-1 是沒設過期時間,-2 是不存在或已過期)
  • DBSIZE(這個 db 有幾個 key)
  • INFO keyspace(每個 db 幾個 key、其中幾個設了 TTL)
  • CONFIG GET hz(背景工作一秒跑幾次,預設 10)

Redis 怎麼刪過期的 key

兩種做法,兩種都不保證即時:

  • 惰性刪除:下次有人來讀這個 key,才發現過期、才順手刪掉。沒人碰就一直佔著。
  • 定期刪除:背景一秒跑 hz 次(預設 10),每次隨機抽 20 個設過 TTL 的 key 檢查,過期的超過 1/4 就再抽一輪。

重點在「隨機抽 20 個」,是抽樣,不是掃全部。下面兩個範例就是同一套機制的兩個極端。

抽不到的那一種

先灌 10 萬個不會過期的 key 當背景
(這兩行在主機的終端機下,不是在 redis-cli 裡面):

docker exec redis30days redis-cli -n 1 FLUSHDB

for i in $(seq 1 100000); do echo "SET bg:$i 1 EX 10000"; done | docker exec -i redis30days redis-cli -n 1 --pipe

-n 1 是切到 1 號資料庫。

再建 100 個各 1MB、一秒後就過期的 key(EVAL 只是拿來灌測試資料,Lua 是之後會遇到的事):

EVAL "local v=string.rep('x',1024*1024) for i=1,100 do redis.call('SET','big:'..i,v,'EX',1) end return 1" 0

DBSIZE   # 100100

記憶體回主機的終端機看:

docker exec redis30days redis-cli INFO memory | grep used_memory_human

https://ithelp.ithome.com.tw/upload/images/20260924/20184209Ibb2eeORmQ.png

這 100 個 key 在第一秒就全部過期了。一分鐘之後再看一次:

DBSIZE

https://ithelp.ithome.com.tw/upload/images/20260924/201842097rJL2IV8Wv.png

docker exec redis30days redis-cli INFO memory | grep used_memory_human

https://ithelp.ithome.com.tw/upload/images/20260924/20184209mpo9buRzw5.png

100100 少到 100085,100 個裡面只被清掉 15 個;記憶體從 139.49M 只掉到 118.21M,還回來 21MB。

算一下就知道為什麼:一秒抽十輪、一輪 20 個,等於一秒只檢查 200 個 key,而這裡一千個裡面才有一個是過期的。一秒抽中 0.2 個,一分鐘十幾個,跟量到的 15 個對得上。

抽得到的那一種

換個極端,10 萬個 key 全部同時過期:

for i in $(seq 1 100000); do echo "SET all:$i 1 EX 1"; done | docker exec -i redis30days redis-cli -n 1 --pipe
DBSIZE   # 100000,剛灌完
DBSIZE   # 0,兩秒後再打一次

這次兩秒內清光。因為隨便抽 20 個都是過期的,遠超過 1/4 那個門檻,Redis 就一輪接一輪抽下去,抽到抽不太到為止。

同一套定期刪除,一邊一分鐘清掉 15 個,一邊兩秒清掉 10 萬個。差別只在「過期的 key 佔整體的比例」,跟過期多久完全無關。

DBSIZE 會騙人

回到上面那個「一分鐘只清掉 15 個」的狀態。KEYS 一個都查不到,DBSIZE 卻照樣把它們算進去:

KEYS big:*   # (empty array)
DBSIZE       # 100085

查詢類的指令會先判斷過沒過期,過期就當作不存在;DBSIZE 和 INFO keyspace 的 keys= 是直接報字典裡有幾筆。

接著讀一個已經過期的 key:

GET big:1    # nil
DBSIZE       # 100084

GET 回 nil 是意料中的,但 DBSIZE 少了 1、記憶體也跟著掉,這就是惰性刪除。那個 key 不是自己消失的,是這次 GET 順手刪掉的。TTL、EXISTS 也會觸發,KEYS 不會。

補一句:惰性刪除和定期刪除都只在主節點上跑,從節點要等主節點的 DEL 複製過來才跟著刪,之後遇到主從節點再來看這件事。

為什麼要在意

  • TTL 到了不等於記憶體還回來,估容量不能假設過期的 key 馬上消失。
  • DBSIZE 是「字典裡還有幾筆」,不是「現在有幾個 key 可以用」,過期沒刪的也算在內。
  • 最危險的是 TTL 設得長、又很少被讀的那種 key(例如暫存 Session、一次性簡訊驗證碼),惰性刪除等不到人來讀,定期刪除抽樣又抽不到。

那如果就是刪不夠快,記憶體先滿了會怎樣?

明天

答案是「看設定」,而且預設值跟大部分人想的不一樣,Redis 預設根本不會自動淘汰任何東西,滿了就直接讓寫入報錯。明天繼續來看 maxmemory 跟八種淘汰策略/images/emoticon/emoticon12.gif


上一篇
Day 11|底層編碼
下一篇
Day 13|記憶體滿了怎麼辦
系列文
從購物車到秒殺:30 天 Redis 高併發自學筆記 共 21 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言