你有一個 DB 當真相來源,又想用快取加速。兩個一起用,核心問題從頭到尾只有一個:怎麼讓快取不要太髒——跟 DB 差太多的快取,比沒有快取更危險,因為它會理直氣壯地回你錯的資料。
四種 pattern 就是四種「管理髒的策略」。先把最重要的講完,再快速掃其他三個——因為實務上你九成的時間只會用第一個。
Cache-Aside:預設答案
讀的路徑:先讀快取 → 命中就回 → miss 就查 DB → 回填快取(設 TTL)→ 回傳。 快取自己不會去載資料,是 app 負責「旁路」地維護它,所以叫 aside。用 Laravel 的話一行就見過它:Cache::remember($key, $ttl, fn () => DB查詢)——這個 helper 包的正是 cache-aside 的讀路徑。
寫的路徑有一條黃金順序:先更新 DB,再「刪掉」快取。 兩個字都有講究——
為什麼是「刪」不是「更新」? 兩個寫入併發時,「更新快取」可能交錯執行、把舊值蓋回去;刪掉最乾淨,下一個讀的人自然會用 DB 的最新值回填。
為什麼「DB 先、快取後」? 反過來的話(先刪快取再寫 DB),中間衝進來的讀者會把還沒更新的舊 DB 值回填快取——你才剛刪完它就又髒了。先讓 DB 成為新真相,再動快取。
然後是教科書比較少講的:這樣做仍然有一個 race。 讀者先從 DB 讀到舊值(還沒來得及回填),此刻寫者完成了「更新 DB + 刪快取」,然後讀者才把手上那個舊值回填進去——快取又髒了。機率低,但長期跑一定遇到。務實的解法不是消滅它,是讓 TTL 當保險:就算髒了,TTL 到期它會自己修正。這就是「回填一定要設 TTL」不是建議、是紀律的原因。
另外三個:知道在解什麼,用到再說
Read-Through——跟 cache-aside 讀起來很像,差在「miss 時去 DB 載資料」這件事由快取層自己做,app 只面對快取。好處是載入邏輯集中;代價是你的快取層(或中介函式庫)要懂怎麼查 DB。本質上是「把 cache-aside 的讀路徑包進基礎設施」。
Write-Through——寫入時同步寫 DB 也寫快取,兩邊一起更新完才回。一致性比較好、快取永遠是熱的;代價是每次寫入都變慢,而且「其中一邊寫失敗」的補償還是要你自己面對。
Write-Behind(回寫)——先寫快取、馬上回,非同步再刷回 DB。寫入快到飛起來;但快取在刷回之前掛掉,那筆資料就沒了。適用場景很窄(吞吐極高、丟一點可容忍的計量類資料),一般業務資料別碰。
| 模式 | 一句話 | 主要代價 |
|---|---|---|
| Cache-Aside | app 自己管快取,讀旁路、寫時刪 | 有上面的 race(靠 TTL 收尾) |
| Read-Through | miss 時快取層自己去載 | 快取層要懂資料來源 |
| Write-Through | 寫入同步寫兩邊 | 寫變慢、雙寫失敗要補償 |
| Write-Behind | 先寫快取、非同步回 DB | 快取掛了會丟資料 |
一致性:沒有完美,只有夠好
快取跟 DB 之間做不到強一致(除非把效能全賠進去),目標是「最終一致、髒的窗口夠短」。按性價比排:
- 短 TTL——底線、必做。所有 race 的最後保險。
- 刪除而非更新——上面講過,降低併發弄髒的機率。
- 延遲雙刪——更新 DB 後刪一次,過幾百毫秒再刪一次,蓋掉 race 窗口裡被舊值回填的那筆。對一致性敏感再上。
- 版本號——回填時帶版本,舊版本不准覆蓋新版本。跟樂觀鎖同一個思路。
務實結論:cache-aside + 短 TTL + 寫時刪快取,這一組合已經覆蓋絕大多數系統。真的更敏感,才逐級往上加。最常見的過度設計是想做「DB 跟快取全自動同步」——那是 invalidation 地獄的入口,還沒進去的不要進去。
講兩句自己的:被髒資料咬到出事的經驗我沒有。但雙寫失敗我碰過——DB 改了、快取沒跟上,兩邊就這樣岔開。而它最麻煩的地方不是修,是我到現在都不知道怎麼重現它:你知道它發生過(兩邊對不上就是證據),但要在測試環境把那個時序重演一次,排不出來。這就是為什麼上面堅持寫時刪一定要配 TTL 兜底——你防不住一個重現不了的 bug,只能讓它的傷害有期限。
一句收束:四個名詞背後是同一個問題——髒的窗口你願意付多少成本去縮短。 90% 的答案是 cache-aside 加短 TTL;剩下 10%,等你真的量到一致性問題再說,不要預付。
(這層邏輯怎麼收進一個 CacheService、寫路徑的 code 長什麼樣,見同系列 #24;TTL 本身怎麼訂,見 #14。)
接下來往哪走
- 為什麼 cache invalidation 是 CS 兩大難題之一 — 「寫時刪快取」的深水區:三種失效法各有一個補不掉的洞
- Outbox Pattern — 雙寫失敗這族病的系統性解法
- 為什麼「加個 cache」常常讓事情更糟 — 髒資料實際咬人的長相,四種翻車認一認