到目前為止,每一種快取模式都接受快取未命中的事實:當金鑰過期、請求未命中,然後有人付出從資料庫重新載入的成本。Refresh-ahead 直接解決這個問題。它不會等到熱門金鑰過期後再重新載入(讓使用者等待),而是在金鑰過期之前,就在背景中主動更新金鑰,讓重要的資料在快取中永遠保持暖狀態。這是一種更進階的模式,適合特定的情境:可預測且頻繁存取的資料,且你永遠不希望使用者遭遇快取未命中。
這是 Redis 大師班的第 12 篇,接續前一篇 write-behind。
expire-then-reload 的問題
在 cache-aside 模式中,快取金鑰會存活到其 TTL 為止,然後過期。下一個請求會未命中,並且必須同步從資料庫重新載入,因此那個不幸的請求會變慢。對於大多數資料來說,這是可接受的,偶爾出現慢速請求是合理的代價。但是對於非常熱門的金鑰(首頁的精選內容、熱門產品、每個請求都會讀取的設定),這種週期性的慢速重新載入會不斷發生,每次都有真實使用者在等待資料庫。更糟糕的是,如果許多請求同時碰到已過期的金鑰,它們會一起重新載入,造成 stampede。
Refresh-ahead 的洞見是:對於你知道會再次被請求的資料,不要等到它過期。在它仍然有效時,就主動在背景中更新它,讓請求永遠命中暖快取。
Refresh-ahead 的運作方式
這個模式會在金鑰的 TTL 耗盡之前,在背景中更新金鑰,因此被提供的數值永遠不會過期到讓使用者遇到未命中的程度。有幾種實作方式。
常見的應用程式層級方法是:當提供快取值時,檢查它距離過期還有多近,如果在更新閾值內,就觸發非同步更新,同時仍然回傳目前的(仍然有效)數值:
async function getWithRefreshAhead(key, ttl, refreshThreshold) {
const remaining = await redis.ttl(key);
const value = await redis.get(key);
if (value && remaining < refreshThreshold) {
// still valid, but expiring soon: refresh in the background, don't wait
queueRefresh(key, ttl);
}
if (value) return JSON.parse(value); // return current value immediately
return await loadAndCache(key, ttl); // cold miss fallback
}
Enter fullscreen mode Exit fullscreen mode
使用者總是能立即從暖快取獲得回應,而更新則在背景進行。金鑰的數值會在原本會過期之前被更新,因此對於熱門金鑰來說,很少發生真正的未命中(伴隨同步重新載入)。
另一種方法是排程:背景工作會定期根據計時器更新已知的一組熱門金鑰,與請求無關。這適合用於固定的關鍵金鑰集合(網站設定、精選清單),你希望無論流量如何都保持它們暖狀態。
成本是什麼
Refresh-ahead 不是免費的,這些成本說明了什麼時候值得使用。你會更新可能不會再次被請求的金鑰,進行投機性的工作,因此你會在沒有人可能需要的資料上花費資料庫查詢。而且它更複雜:你需要背景更新機制、閾值邏輯,並且要小心避免同時更新同一個金鑰多次。
這種投機性工作的成本就是為什麼 refresh-ahead 是針對性的,而不是預設的模式。將它應用到每個金鑰上會因為冷資料的主動更新而淹沒資料庫。它只對真正熱門且可預測的金鑰有回報,因為更新工作幾乎總是有用的,因為金鑰真的會再次被請求。
什麼時候使用它
Refresh-ahead 適合狹窄但有價值的案例:
- 持續存取的熱門金鑰:內容、設定,或位於大多數請求關鍵路徑上的資料,未命中會影響許多使用者。
- 可預測的存取:你可以自信地說資料很快就會再次被請求,因此主動更新不會浪費。
- 計算成本高的數值:當重新載入成本高昂(繁重的查詢或計算)時,避免同步未命中特別值得。
對於很少存取的金鑰長尾來說,這是多餘的,因為 cache-aside 的 load-on-miss 整體來說更便宜,因為這些金鑰大多數在過期之前都不會再次被請求。
與其他模式的結合
Refresh-ahead 不是 cache-aside 的替代品,而是建立在它之上的針對性增強。典型的設定是對大部分快取使用 cache-aside,然後對一小組已識別的熱門金鑰加入 refresh-ahead。你可以在所有地方獲得 cache-aside 的簡單性和優雅降級,同時在最重要的地方獲得 refresh-ahead 的未命中避免。它也直接幫助解決 stampede 問題:主動在熱門金鑰過期之前更新它,意味著它永遠不會在負載下過期,因此同時未命中的群體永遠不會形成。
Refresh-ahead 是讓特定熱門資料永遠保持暖狀態的模式。在金鑰過期之前在背景中更新金鑰,讓使用者永遠不會等待重新載入,接受你正在進行投機性工作,並且只將它應用於可預測、高流量的金鑰,在這些金鑰上工作才有回報。對於這些金鑰,它將週期性的慢速重新載入轉變為持續快速的回應。
這涵蓋了核心快取模式。接下來,我們要解決它們都共有的問題:快取失效,這是快取真正困難的部分,以及保持快取資料正確的策略。
重點摘要
- Refresh-ahead 會在熱門金鑰過期之前在背景中更新它們,因此使用者永遠不會付出同步重新載入的代價。
- 實作方式是在被提供的金鑰距離過期在閾值內時進行更新,或為已知的熱門金鑰排程更新。
- 成本是投機性工作:你會更新可能不會再次被請求的金鑰,因此它只對可預測的熱門資料有回報。
- 將它應用於一小組熱門、可預測、計算成本高的金鑰,而不是很少存取資料的長尾。
- 它建立在 cache-aside 之上,並透過確保熱門金鑰永遠不會在負載下過期來幫助防止 stampede。
常見問題
什麼是 refresh-ahead 快取?
一種在金鑰過期之前主動在背景中更新快取金鑰的模式,因此頻繁存取的資料保持暖狀態,請求永遠不會遇到同步重新載入。它以一些投機性工作換取避免熱門金鑰的快取未命中。
Refresh-ahead 與 cache-aside 有什麼不同?
Cache-aside 只在金鑰過期且請求未命中後才重新載入金鑰,因此該請求會變慢。Refresh-ahead 會在熱門金鑰過期之前重新載入它們,在背景中進行,因此使用者永遠會命中暖快取。Refresh-ahead 通常建立在 cache-aside 之上,針對選定的金鑰。
我什麼時候應該使用 refresh-ahead?
對於位於關鍵路徑上的一小組熱門、可預測金鑰(網站設定、精選內容、熱門項目),特別是當重新載入它們成本高昂時。對於很少存取的金鑰來說不值得,因為主動更新工作會被浪費。
Refresh-ahead 的缺點是什麼?
它會進行投機性工作,更新可能不會再次被請求的金鑰,如果應用於冷資料,會浪費資料庫查詢。它也更複雜,需要背景更新邏輯,並且要小心避免重複更新。
Refresh-ahead 有幫助解決快取 stampede 嗎?
有。透過在熱門金鑰過期之前更新它,金鑰永遠不會在負載下達到過期狀態,因此當熱門金鑰過期時發生的許多同時未命中 stampede 不會形成。
延伸閱讀
本文最初發表於 amanksingh.com/blog/redis-refresh-ahead。
關於作者
Aman Kumar Singh 是位於印度諾伊達的團隊主管兼高級軟體工程師,撰寫關於全端工程、系統設計,以及使用 TypeScript、Next.js、NestJS、PostgreSQL 和 Redis 的生產級 SaaS 的文章。
- 作品集:amanksingh.com
- GitHub:amansingh1501
- LinkedIn:amansingh1597
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.