秒殺的時候,所有人都在搶同一個 stock:1,這種被很多人同時讀寫的 key 叫熱 Key。另一種是裡面裝了太多東西的 key,叫大 Key。
KEYS * 一個指令卡住所有人,大 Key 也一樣:對它下一個指令,裡面有幾筆就要處理幾筆。今天先看大 Key 怎麼找、怎麼刪,再看熱 Key。
先做兩個大 Key,各塞 100 萬筆,例如把一個活動所有參加的人都放在同一個 Set。等一下一個用 DEL 刪、一個用 UNLINK 刪(在主機的終端機下):
# big:1、big:2 各塞 100 萬筆,--pipe 批次送
docker exec -i redis30days sh -c 'awk "BEGIN{for(i=0;i<1000000;i++) printf \"SADD big:1 m%d\r\nSADD big:2 m%d\r\n\", i, i}" | redis-cli --pipe'
接著找出來,再看它佔多少記憶體(在主機的終端機下):
docker exec redis30days redis-cli --bigkeys
docker exec redis30days redis-cli MEMORY USAGE big:1


--bigkeys 會列出每種型別最大的 key,Set 最大的就是剛剛灌的,有 1000000 筆。它是用 SCAN 分批掃,不會像 KEYS * 一次卡住所有人。
MEMORY USAGE 是這個 key 佔了多少 bytes,48388712 差不多是 46MB,只是一個 key。--bigkeys 比的是筆數,想照記憶體大小排,可以換成 --memkeys。
刪一個 key 看起來很簡單,但 DEL 要把裡面 100 萬筆一筆一筆釋放掉,這段時間其他人都在等。用慢查詢日誌看花了多久(在主機的終端機下):
docker exec redis30days redis-cli SLOWLOG RESET
docker exec redis30days redis-cli DEL big:1
docker exec redis30days redis-cli UNLINK big:2
docker exec redis30days redis-cli SLOWLOG GET

慢查詢日誌會記下超過 10 毫秒的指令(slowlog-log-slower-than 預設 10000 微秒),SLOWLOG RESET 是先把之前的清掉。
DEL big:1 被記下來了,DEL 上面的 87589 是它花了多久,單位是微秒,差不多 88 毫秒。跟 KEYS * 一樣,這 88 毫秒裡其他人的指令都在排隊。
UNLINK big:2 沒有被記下來,不到 10 毫秒就回了。它不當場釋放:先把 key 拿掉,裡面的 100 萬筆丟給背景慢慢釋放。
所以刪大 Key 用 UNLINK。也可以把 lazyfree-lazy-user-del 設成 yes(預設是 no),DEL 就會跟 UNLINK 一樣丟給背景。
慢查詢日誌只留最近 128 筆(slowlog-max-len),重開就沒了。
大 Key 是一個 key 太大,熱 Key 是一個 key 被打太多次。找熱 Key 用 --hotkeys,但直接下會噴錯(在主機的終端機下):
docker exec redis30days redis-cli --hotkeys

--hotkeys 看的是 OBJECT FREQ,要先換成 LFU,Redis 才會幫每個 key 算被碰了幾次。
maxmemory 也要一起設。沒設的話,Redis 為了省記憶體,值一樣的小數字(0 到 9999)只存一份,大家共用,被碰的次數也算在一起。stock:1 扣到 0 之後,其他值也是 0 的 key 就算沒人碰過,也會跟它一樣熱。1gb 只是設一個夠大、不會開始淘汰的上限。
Java 開著,用 deduct.js 打 5000 次 stock:1(在主機的終端機下):
docker exec redis30days redis-cli CONFIG SET maxmemory 1gb
docker exec redis30days redis-cli CONFIG SET maxmemory-policy allkeys-lfu
docker exec redis30days redis-cli SET stock:1 100
./k6/run.sh deduct
docker exec redis30days redis-cli --hotkeys

掃了 10005 個 key,只找到 stock:1,counter 是 37。跟 OBJECT FREQ 一樣是取對數,不是真的次數,所以 5000 次只有 37。
測完記得還原(在主機的終端機下):
docker exec redis30days redis-cli CONFIG SET maxmemory 0
docker exec redis30days redis-cli CONFIG SET maxmemory-policy noeviction
Redis 一次只處理一個指令,一個 key 再熱也只有一台在扛。換成 Cluster 也沒用:一個 key 只會落在一個 slot,其他台再閒也分不到。
常見的兩種做法:
CacheBuilder。代價是這幾秒可能讀到舊的。stock:1 的 100 件,拆成 stock:1:0~stock:1:9 10 個 key,每個 key 放 10 件,加起來還是 100 件,每個人隨機挑一個扣。這 10 個 key 會分到 10 個不同的 slot,Cluster 上就能分給好幾台扛。代價是挑到的那個扣完了,要換一個再試。DEL,SMEMBERS 這種整包拿的指令一樣要處理 100 萬筆,跟 LRANGE 0 -1 是同一個問題。能拆就拆成好幾個小 key,要整包看就用 SSCAN 分批拿。KEYS、FLUSHALL 這種一下就出事的指令,正式環境通常會用 ACL 讓 App 的帳號不能下。今天的問題都是出事了才去找。明天來看平常要盯哪些數字,在出事之前就看出來![]()