【從一台 Server 到分散式架構】第 05 篇:常被問的菜單寫在小黑板——Redis 與快取

上一篇我們聊到,當小明他們的課程平台上線人數變多,資料庫查詢變慢時,最有效也最省錢的第一步是「優化資料庫體質」,加上適當的索引並改寫沒有效率的查詢語句。

經過那波優化,伺服器確實穩定了好一陣子。但隨著小明平台的名氣越來越大,甚至開始有知名講師進駐開課,他們遇到了一個新問題:首頁的「熱賣排行」跟「精選講師資訊」被瘋狂地查詢。

想像一下,每秒鐘有幾千個訪客進到首頁,後端程式就會乖乖地去問資料庫:「請問現在賣得最好的是哪五堂課?」資料庫哪怕已經加了索引,還是要在一大堆訂單與課程表之中做計算、做關聯,然後回傳結果。

但這五堂「熱賣課程」,每一秒鐘的結果根本就一模一樣啊!為了這幾乎不變的結果,卻讓最核心的資料庫每秒鐘重複計算幾千次,這不僅是資源的浪費,也是讓系統陷入危機的導火線。


快取的直覺:常被問的菜單寫在小黑板

如果把資料庫比喻成餐廳裡那本厚厚的大帳本與菜單總匯,每次客人進來問「今天有什麼特價菜?」,店員都要翻開大帳本查一次,不僅店員累,客人也排隊排得很長。

這時候,聰明的店長會在門口擺一塊小黑板。 店長把今天最熱門的「特價菜單」直接抄在小黑板上。當客人進來問特價菜,店員連翻帳本都不用,直接指著小黑板說:「都在上面,自己看。」

在系統設計裡,這個「小黑板」就是快取(Cache),而在實務上,最常被拿來當這塊小黑板的工具之一,就是 Redis

Redis 是一個跑在記憶體(Memory)裡的資料儲存系統。相比於寫在硬碟裡的資料庫(如 MySQL 或 PostgreSQL),讀取記憶體的速度快了不只一個數量級。我們把後端程式查出來的「熱賣排行」存進 Redis,接下來的幾千個查詢,後端不用再去煩資料庫,直接去 Redis 拿資料就回傳給前端。

有了這塊小黑板,首頁載入速度瞬間從幾百毫秒掉到了幾毫秒,資料庫的 CPU 也瞬間獲得了解放。


什麼資料適合寫在小黑板上?

既然小黑板這麼好用,那我們把「所有的」資料都抄上黑板不就好了? 當然不行。因為記憶體很貴,小黑板的空間很有限;而且黑板上的資料如果一直變,店員整天在那邊擦黑板、重寫黑板,反而更沒效率。

所以,通常只有這兩類資料適合放進快取:

  1. 讀多寫少,且變動不頻繁的資料:例如課程介紹、熱門文章清單、平台共同設定。這些資料可能幾天才改一次,但每秒都被瘋狂讀取。
  2. 計算極度耗時的結果:例如經過複雜聚合運算出來的「年度銷售總排行榜」。這種只要算好一次,存進快取讓大家直接看結果就好,不用每次重新算。

什麼不適合放? 像使用者的「私人對話紀錄」或是「剛下單的付款狀態」,這種每秒都在變、而且只有特定一個人會看的資料,你把它寫上黑板既沒有共用效益,還會為了維持資料正確性而搞死自己。


黑板何時要擦掉?—— 快取的過期與失效策略

小黑板最大的致命傷在於:如果帳本(資料庫)已經改了,但小黑板沒改,客人就會看到舊菜單。

這在系統設計裡叫做「資料一致性問題」。實務上,小明他們有幾種策略來決定什麼時候把黑板擦掉換新:

1. 給資料一個保存期限(TTL, Time to Live)

最簡單暴力的做法。小明在把熱門排行寫進 Redis 時,設定這筆資料「存活 10 分鐘」。 10 分鐘一到,Redis 自動把這筆資料刪掉。下一個使用者連進來,發現黑板空了(Cache Miss),後端就會跑去問真正的資料庫,算完後回傳給使用者,同時把「最新的」熱門排行再寫進 Redis,並重新倒數 10 分鐘。 就算這 10 分鐘內資料庫的排名變了,訪客看到舊的排行也無傷大雅。這種**「允許短時間內資料不一致,一段時間後會自我修正」的策略,在業內稱為最終一致性(Eventual Consistency)**的一種體現。

2. 主動刷新(Cache Invalidation)

如果某個資料非常重要,不能有 10 分鐘的落差(例如講師立刻把課程下架,首頁就絕不能再賣),那就在後端程式裡寫一段邏輯: 「當資料庫的課程狀態被更新時,同時去把 Redis 裡的那筆資料刪除或覆寫。」 這能確保最高的一致性,但也代表後端的程式碼會變得比較複雜,幾乎每個寫入的動作都要考慮到「要去哪裡刪快取」。


快取世界的三大危機:打穿、雪崩、擊透

把系統架上 Redis 看似解決了效能問題,但如果不了解它的副作用,反而會引發更災難性的後果。很多面試愛考名詞,其實都是因為這幾張小黑板出事了:

1. 快取穿透(Cache Penetration) 這是指有人一直來查一個「根本不存在」的東⻄(例如亂猜課程 ID = 99999999)。因為黑板上當然沒有查不到的東西(Cache Miss),所以後端就跑去查大帳本(資料庫),結果大帳本裡也沒有。 如果駭客一秒鐘發出一萬個這種惡意請求,所有請求就會「穿透」小黑板而直接重擊資料庫。 解法:對於資料庫查不到的東⻄,也把它寫上黑板並標記為 null 或「空值」。你心裡可能會有個疑問:那如果明天這堂課真的被建出來了,快取不就一直以為「沒有這堂課」嗎?沒錯,所以這個「空值」快取通常會設定一個非常短的存活時間(例如 30 秒)。駭客通常是一秒鐘打幾千次,我們只要讓這個 null 存活 30 秒,就能擋下這段時間內的幾萬次攻擊,保護資料庫。30 秒後快取消失,大不了再查一次 DB,發現還是沒有,就再擋 30 秒。而如果是正常的系統流程新增了這筆資料,後端寫入資料庫成功的同時,就會由程式主動去把這個空值快取殺掉,所以使用者下一秒來查,就會正常去資料庫撈出熱騰騰的新資料了。除了存空值,有些規模更大的系統會用布隆過濾器(Bloom Filter)來快速判斷資料存不存在,這裡先不展開。

2. 快取雪崩(Cache Avalanche) 記得剛剛提到的 TTL 嗎?假設小明很懶,把所有熱門課程、講師名單的 TTL 都設定為每天半夜 12 點過期。 在 12:00:00 那一瞬間,黑板上「所有」的重要資訊同時被擦光。這時候進來的龐大流量發現黑板全空,全部一起轉頭湧向資料庫去查帳本。這座平時都靠小黑板保護的資料庫,瞬間承受不了暴增的壓力而崩潰,這就是雪崩。 解法:不要讓大家的過期時間一樣。在設定 TTL 時,加上一個隨機的亂數(例如加上 1~5 分鐘),讓黑板上的資料「分批」過期,錯開查 DB 的高峰。

3. 快取擊穿(Cache Breakdown / Hotspot Invalid) 這和雪崩有點像,但是是針對單一一個極度熱門的資料。 假設晚上 8 點有超級名師直播開賣課程,幾萬人同時在點這堂課。偏偏在 8:00:05 的時候,這個課程的快取「剛好過期」了。 在快取過期、但新的快取又還沒寫進去的那短短幾十毫秒內,這幾萬個請求發現黑板沒字,全都衝向 DB 要查這堂課,瞬間把 DB 打趴。 解法:針對這種極度熱門節點,除了不要隨便讓它過期外,實務上常在程式層面加上「互斥鎖(Mutex / Lock)」。當發現快取過期時,只放行「第一個」請求去查庫跟寫黑板,其他人在外面稍微排隊等一下,等第一個寫好黑板了,其他人再來看現成的。


小結與預告

當資料庫的查詢已經無法再靠索引優化時,我們引入了 Redis 這塊小黑板(快取)。把讀多寫少、耗時計算的熱門資料放在記憶體裡,可以擋掉 90% 以上的資料庫壓力。但同時我們也必須面對資料一致性的取捨,以及防範駭客或大流量帶來的打穿與雪崩危機。

有了 Redis 擋在前面,小明他們的課程平台又順利度過了一次危機。不過,隨著課程與訂單每天持續產生,帳本裡的資料越來越龐大;而且,就算讀取的壓力被 Redis 擋掉了大半,**「每個人買課新增訂單、寫評論、更新狀態」**這些需要真正寫入帳本的動作,依然全擠在同一台資料庫上。

大家都要寫同一本帳本,開始要大排長龍了。 既然一本帳本來不及寫,我們能不能多買幾本?但多買幾本,要怎麼確保大家的帳目不打架?這就是我們下一篇要談的:共用一本帳本太慢——讀寫分離與主從複製。我們下一篇見。