Redis -

鎖定
一般的快取流程(單純快取)
步驟是這樣的:
先去 Redis 找資料 X,有的話直接回傳。
如果 Redis 沒有,就去主資料庫拿資料。
拿到後再放回 Redis。
回傳給使用者。
看起來很正常吧?
但問題在於 「快取缺席」的那一瞬間。
問題場景
假設同一時間有 1000 個人同時來查詢資料 X。
如果資料庫只要 80ms 就能回傳,這 1000 個人雖然 Redis 沒資料,但資料庫還能勉強頂住(同時湧入 80 個請求,壓力還算可控)。
如果資料 X 需要計算 8 秒(8000ms),那麼在 Redis 還沒被「補上」之前,這 1000 個人都會同時衝進資料庫,結果資料庫要處理 8000 個請求 × 8 秒 = 64000 秒 CPU 時間。這時候你的資料庫直接爆炸,整個系統癱瘓。
這就是為什麼你約會的時候還要拿筆電救火的原因:表面上流程合理,但遇到高併發 + 重運算,會出現「快取雪崩」的災難。
進階的快取流程(加鎖)
改良的方法是「加鎖」,步驟是這樣:
先去 Redis 找資料 X,有的話直接回傳。
如果沒有,就先拿到「資料 X 的鎖」,確保只有我能進去查主資料庫。
在鎖住的情況下,再次檢查 Redis(因為也許剛好別人已經放進去了)。有的話直接回傳。
如果還是沒有,再去主資料庫拿資料 X。
拿到後放回 Redis。
回傳資料,並釋放鎖。
改良效果
同一時間的 1000 個請求,只有 1 個人會真的去資料庫。
其他 999 個人會等鎖釋放,等到資料被放進 Redis 後,直接從 Redis 拿現成的結果。
這樣資料庫就不用承受「雪崩式的瞬間流量」。
consistency
node = hash(key) % n
- 把每個 key 先做雜湊(例如 MD5),得到一個很大的整數 h。
- 把它對目前節點數 n 取餘數:h % n,餘數介於 0 ~ n-1,就當成它要去的第幾台 Redis。
當 n 改變時:
| h | h%3 → 舊節點 | h%4 → 新節點 | 是否搬家 |
|---|---|---|---|
| 0 | 0 | 0 | 否 |
| 1 | 1 | 1 | 否 |
| 2 | 2 | 2 | 否 |
| 3 | 0 | 3 | 是 |
| 4 | 1 | 0 | 是 |
| 5 | 2 | 1 | 是 |
| 6 | 0 | 2 | 是 |
| 7 | 1 | 3 | 是 |
| 8 | 2 | 0 | 是 |
| 9 | 0 | 1 | 是 |
| 10 | 1 | 2 | 是 |
| 11 | 2 | 3 | 是 |
結果:只有 3/12(25%)沒搬家,9/12(75%)全部搬了。
這不是個案,一般情況從 n 增加到 n+1 時,約有 n/(n+1) 的鍵會換目的地:
從 3→4 台:大約 75% 會搬。
從 10→11 台:大約 90.9% 會搬。
從 100→101 台:大約 99% 會搬。
因為把「除數」從 n 改成 n+1,對任意雜湊值 h,新餘數幾乎跟舊餘數沒有穩定關係;要剛好落回同一台的機率只有 1/(n+1)。
位移 ≈ 大量 Key 搬家:原本在 A 機上的資料,改完公式後,跑去 B/C/新機,新機器上沒有那些值 → 全部成為快取未命中(miss)。導致瞬時回源風暴:大量 miss 會同時打到你的 DB/主資料來源,造成「雪崩(cache stampede)」。因此造成效能曲線崩塌:你擴容本來是想救火,結果因為 rehash,先挖了一個更大的洞。
Consistent Hashing
先從「傳統的 mod」說起
想像你有 10 台冰箱,要把「100 種飲料」平均放進去。
你用的方法是:
👉 冰箱編號 = 飲料名字.hash % 10
結果很平均。但有一天朋友又買了一台冰箱,第 11 台。
現在規則變成:
👉 冰箱編號 = 飲料名字.hash % 11
問題來了:
幾乎所有飲料都要被「重新分配」到不同的冰箱。
原本冰箱裡的東西都要搬家。
你想找「可樂」卻在舊冰箱裡找不到,因為它已經被算到別的冰箱去了。
這就是為什麼叫做 「快取全滅」。
換個想法:
不要用「% 冰箱數」來決定飲料去哪,而是 先把所有冰箱和飲料都排到一個圓環上。
圓環怎麼排?
假設 Hash Value 範圍是 0 ~ 2^32-1,把它想像成一個鐘面(環形)。
每台冰箱(節點)也算出一個 Hash Value → 定位到鐘面上的某個點。
每瓶飲料(key)也算出 Hash Value → 定位到鐘面上的某個點。
飲料順時針找最近的冰箱 → 放進去。
發生什麼事?
如果加一台新冰箱(例如放在鐘面 3 點方向),只會影響「它前面到它之間」那段飲料,其他飲料不動。
所以不是「全部重算」而是「部分移動」。
舉例
原本有 3 台冰箱,落在鐘面 12 點、4 點、8 點。
可樂算出來在 2 點,所以它往順時針走 → 存在 4 點冰箱。
現在新增一台冰箱,在 3 點。
👉 只有「2 點 ~ 3 點之間」的飲料會搬去新冰箱,可樂剛好在這區,所以搬家。
👉 其他例如「6 點的雪碧」,還是順時針到 8 點冰箱,完全沒事。
所以擴容只會影響一小部分資料,不會大規模「全滅」。
Redis Cluster 怎麼做?
Redis Cluster 沒有直接用圓環,而是固定先把整個範圍切成 16384 個 slot。每個 key 算出 slot → 分配給某台 Redis。擴容時,只需要把部分 slot 從舊機器搬到新機器。slot 數永遠固定(16384),所以 key → slot 的公式不會因為機器數改變而變。
效果跟 Consistent Hashing 一樣,新增/移除節點,只搬少量資料。
「瘋狂的 TTL」為什麼會讓 Redis 塞滿過期資料?
發生什麼, 你對大量 key 設了 TTL(可能太長、或數量暴增),一段時間後,同一時間窗 會有很多 key 到了有效期。但 Redis 的刪除不是逐一準時執行,而是「背景抽樣 + 惰性刪除」。結果:大量已過期但尚未刪除 的 key 仍占用記憶體。
為什麼
背景掃描是抽樣與有配額的(避免把 CPU 全拿去掃 TTL),掃不完就留到下輪。
若你的 TTL 分布「集中到期」或總量過大,背景掃描來不及清,僵屍鍵會在記憶體裡堆積。
會影響什麼
記憶體被過期垃圾占住,還沒到業務可用的熱資料,就先被「垃圾」擠壓。
很快逼近 maxmemory,下一次寫入 就不得不觸發 eviction。
設計 TTL 的原則
避免「集中到期」:讓 TTL 帶一點隨機抖動(如 EX 600 + rand(0~60)),分散到期尖峰。
避免「過度短 TTL + 高寫入頻率」:太短會變成「寫入風暴」,加重逐出與釋放壓力。
避免「過長 TTL + 大量 key」:太長又容易囤出一堆僵屍鍵,逼近 maxmemory。
預估容量:以「高峰同時存活 key 數 × 平均 value 尺寸 × 安全係數」去訂 maxmemory 與 TTL。


