快取讓系統快,但也讓「改了卻沒生效」成為最難 debug 的一類問題。當同一份品牌事實躺在 6 層快取裡,「deploy 完成」不等於「訪客看到」。本章記錄如何讓一次寫入,在 1 萬租戶尺度下數秒內穿透所有快取層。
平台的一次「品牌事實變更」(admin 改 FAQ、scanner 抓到官網新 URL、cron 重生 AXP)要反映到 AI 爬蟲看到的公開檔(sitemap.xml、schema.json、llms.txt),中間隔著多層快取。若只清最靠近的一層,其餘層仍吐舊值 — 表面上「deploy 成功」,實際訪客與爬蟲看到的是 stale 內容。
在 1 萬租戶尺度,這個延遲被放大:若傳播依賴各層 TTL 自然過期,最慢一層(CF edge 1 小時)就決定了整體傳播延遲。對「客戶今天改了官網,希望 AI 明天就看到新內容」的期待而言,1 小時 × 各層疊加是不可接受的。
目標:把 P50 傳播延遲從「1 小時(自然過期)」壓到「數秒(主動失效)」,且不依賴 admin 手動按重新整理。
先誠實盤點資料在哪些層被快取:
| 層 | 位置 | TTL | 失效方式 |
|---|---|---|---|
| Browser | 訪客瀏覽器 | Cache-Control: max-age |
hard reload |
| L3 CF edge | Cloudflare 邊緣 | 5min–24hr 依 content-type | 自然過期 或 CF API purge |
| CF Worker subrequest | Worker 內 cf:cacheTtl |
robots 1hr / schema 6hr 等 | Worker redeploy 或 CF purge |
| Worker 內 in-memory | CF Worker 執行環境 | 5min | Worker redeploy |
| L1 Backend Redis | 東京主站 | 5min(axp_public_data:v1:{slug}) |
主動 del 或 自然過期 |
| Backend in-memory | backend process | 5min | container restart |
其中兩層是主動失效的主要施力點:L1 Backend Redis(資料源頭最近的快取)與 L3 CF edge(訪客 / 爬蟲最近的快取)。把這兩層打通,中間的 Worker / browser 層以短 TTL 收斂即可。
核心是一個 cascade:任一品牌寫入 → 清 L1 → 連鎖清 L3。
flowchart TD
A[品牌寫入<br/>admin / scanner / cron] --> B[invalidatePublicDataCache brandSlug]
B --> C[L1: Redis del axp_public_data:v1:slug<br/>即時]
B --> D[L3: purgeCfEdgeCache<br/>fire-and-forget]
D --> E[精準清 9 個公開檔 URL<br/>sitemap/robots/llms/schema/feed/brand-faq]
E --> F[T+5 sec 傳播完成]
Fig 19-1:一次品牌寫入觸發 L1 Redis 即時清 + L3 CF edge 精準 purge。
purgeCfEdgeCache 精準清 9 個公開檔的完整 URL(buildPublicFileUrls 依 brand website 組出 sitemap.xml / sitemap-axp.xml / sitemap-index.xml / robots.txt / llms.txt / llms-full.txt / schema.json / feed.xml / brand-faq.json),而非全站 purge。
兩個工程紀律:
buildPublicFileUrls — 任何新公開檔若漏加,主動 purge 就漏它一層,又回到「靠自然過期」。不同公開檔的變動頻率不同,TTL 應該按 content-type 分級,而非一刀切。TTL 由單一 SSOT(ttlForContentType)決定:
| Content-Type | TTL | 適用 | 理由 |
|---|---|---|---|
application/xml |
5 min | sitemap.xml 系列 | URL list 易變 |
application/rss+xml |
5 min | feed.xml | 更新訂閱需即時 |
application/ld+json |
1 hr | schema.json | brand entity 相對穩 |
application/json |
1 hr | brand-faq.json | 中頻 |
text/plain |
24 hr | robots.txt / llms.txt | 設定極穩 |
一個關鍵的一致性要求:CF Worker template 的 cf:cacheTtl 必須對齊這份 backend SSOT。若 backend 說 sitemap 5min 但 Worker 快取 300 秒以外的值,兩層會打架。這條 SSOT 讓「TTL 縮短」這種調整只改一處。TTL 是主動 purge 失敗時的兜底:即使 L3 purge 因 CF API rate limit 失敗,最壞情況也只 stale 一個 TTL 週期(sitemap 5min),而非 1 小時。
主動失效解決了「平台內部寫入」的傳播,但還有一個缺口:客戶自己改了官網,平台怎麼知道?
答案是把失效 cascade 接進 scanner,並讓 scanner 定期自動跑:
sitemapScanner.scanSitemap 抓完客戶官網、更新 brand_content_pages 後,連鎖呼叫 invalidatePublicDataCache(brandSlug),自動觸發 L1 + L3 清除。客戶官網的新 URL 因此在 scan 後數秒反映到平台 sitemap。scan-brand-site(per-brand stagger 避免 origin rate limit)。搭配上一點,客戶 origin 變動在 T+1 天內 zero-touch 傳播,無需客戶或 admin 做任何操作。flowchart LR
A[T+0 客戶官網加新 URL] --> B[daily scan cron<br/>per-brand stagger]
B --> C[scanSitemap 更新 brand_content_pages]
C --> D[chain invalidatePublicDataCache]
D --> E[L1 Redis del + L3 CF purge]
E --> F[T+5s 平台 sitemap 含新 URL]
F --> G[AI bot 下次 crawl 看到新 URL]
Fig 19-2:客戶 origin 變動的 zero-touch 傳播全鏈。
一個 stagger 的規模化細節:daily cron 的 per-brand 延遲若設 5 分鐘,1 萬租戶要跑 34 天才跑完一輪 — 遠超 daily。因此 stagger 縮到 30 秒級,讓一輪在單日內完成。這類「per-brand 迴圈的總時長 = 延遲 × 租戶數」的算術,是 1 萬租戶尺度必須隨時心算的約束。
zero-touch 覆蓋率的實務意義:在這套機制之前,約半數傳播依賴 scanner 偶發觸發;之後,常態變動(平台內部寫入 + 客戶 origin 每日 polling)都自動傳播,只剩「客戶臨時要求立即加某 URL」才需 admin 手動介入。
L3 edge purge 需要 CF API token。1 萬租戶下 token 的權限範圍(scope)是一個容易做錯的設計點:
| Token scope | 問題 |
|---|---|
| 單一 zone 的 Cache Purge | 每加一個客戶 zone 都要回 CF Dashboard 改 token policy — 1 萬租戶不可行 |
| 全帳號所有 zone(All zones) | 範圍過大,不必要的風險 |
| Account 中的所有區域 + Cache Purge | 正確:同帳號下任一客戶 zone 加入後,purge zero-touch 立即生效 |
正確設計是 token 授予「Account 中的所有區域」的 Cache Purge 權限。如此,任何客戶 zone 只要納入同一 CF Account,L3 purge 就自動 cover,不需 per-tenant token、不需每加 brand 改 policy。
一個設計性限制要誠實記錄:若客戶堅持在自己的 CF Account 持有 zone(不轉入平台 account),則平台 token 無法 purge 該 zone,需走 per-tenant token(客戶 onboarding 時提供自家 CF token)。這是架構邊界,非 bug。
快取失效 5 層架構的核心價值:用 L1 Redis 主動清 + L3 CF edge 精準 purge + L4 頻率感知 TTL 兜底 + scanner 自動失效 + daily polling,把 1 萬租戶的內容傳播從「靠自然過期的 1 小時」壓到「數秒的 zero-touch」,且不依賴任何人手動觸發。
cf:cacheTtl 對齊;TTL 是 purge 失敗時的兜底。cf.cacheTtl request option。| 日期 | 版本 | 說明 |
|---|---|---|
| 2026-07-06 | v1.2 | 初稿。記錄六層快取盤點、L1/L3 主動失效 cascade、頻率感知 TTL SSOT、scanner 自動失效 + daily polling、CF token scope 設計。 |