慣性聚合 高效追蹤和閱讀你感興趣的部落格、新聞、科技資訊
閱讀原文 在慣性聚合中打開

推薦訂閱源

博客园 - 司徒正美
V
V2EX
T
Tailwind CSS Blog
有赞技术团队
有赞技术团队
aimingoo的专栏
aimingoo的专栏
Apple Machine Learning Research
Apple Machine Learning Research
IT之家
IT之家
Blog — PlanetScale
Blog — PlanetScale
A
About on SuperTechFans
月光博客
月光博客
T
The Blog of Author Tim Ferriss
宝玉的分享
宝玉的分享
Martin Fowler
Martin Fowler
博客园 - 聂微东
The GitHub Blog
The GitHub Blog
V
Visual Studio Blog
WordPress大学
WordPress大学
酷 壳 – CoolShell
酷 壳 – CoolShell
Engineering at Meta
Engineering at Meta
GbyAI
GbyAI

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python)
2026年的緩存層:CDN、應用程式、資料庫、查詢:什麼放在哪裡
Gabriel Anha · 2026-05-24 · via DEV Community

有四層快取位於使用者請求與他們查詢的資料列之間。大多數團隊只使用其中兩層(通常是將 Redis 放在資料庫前面,再加上用於靜態資產的 CDN),而將另外兩層視為別人的問題。

那樣你就會得到一個 Redis 層級在做 CDN 應該做的工 作,一個靜默地使用其計劃緩存作為品質墊的數據庫,以及每當一個熱鍵過期時發生的擁擠。每一層回答一個不同的問題。選擇錯誤的層級,你就得為你沒選擇的那一層付出代價。

為何緩存是分層的,而不是單一選擇

模式很重要,因為問題會疊加:

  • CDN 回答「我們可以完全避免查詢來源嗎?」
  • 應用程式快取回答「我們可以避免資料庫迴圈查詢嗎?」
  • 資料庫快取回答「我們可以避免重新計算結果嗎?」
  • 查詢快取回答「我們可以避免解析和規劃陳述式嗎?」

每個 "是" 都會短路下方的層。每個 "否" 都會將請求傳遞下去。一個觸及所有四層的請求,除了成本和延遲之外,不應該感覺與一個未觸及任何層的請求不同。

錯誤在於將其壓縮為單一層。團隊將所有內容都丟進 Redis,因為那是他們已經擁有的層。Redis 是一個不錯的應用程式快取。它是一個糟糕的 CDN,一個更糟的實體化視圖,而且它完全無法幫助你的準備陳述式規劃器。

層級 1:CDN 輪廓快取

CDN 位於用戶最近的地方。它是服務請求最便宜的地方,因為請求永遠不會到達您的伺服器。2026年的技巧在於CDN會緩存超過 .jpg 個檔案。如果您告訴它們如何,它們還會緩存API回應。

這是一個Cloudflare對公共產品列表端點的緩存規則:

// Cloudflare Worker - cache /api/products/* responses for 60s at edge
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    const cache = caches.default;

    // only cache GETs for the public catalog
    if (request.method !== "GET" ||
        !url.pathname.startsWith("/api/products/")) {
      return fetch(request);
    }

    // strip auth-affecting query params from the cache key
    url.searchParams.delete("trace_id");
    const cacheKey = new Request(url.toString(), request);

    let response = await cache.match(cacheKey);
    if (response) {
      return response; // edge hit, never touches origin
    }

    response = await fetch(request);
    if (response.status === 200) {
      const cached = new Response(response.body, response);
      cached.headers.set("Cache-Control", "public, max-age=60, s-maxage=60");
      cached.headers.set("CDN-Cache-Control", "max-age=60");
      ctx.waitUntil(cache.put(cacheKey, cached.clone()));
      return cached;
    }
    return response;
  },
};

輸入全螢幕模式 退出全螢幕模式

這是s-maxage 是共用快取指令。它告訴CDN要保留回應多久,與瀏覽器所做的無關。兩個 Cache-Control 指令讓你可以給CDN 60秒,同時告訴瀏覽器不同的內容(通常為 max-age=0, must-revalidate)。

這裡放什麼:公開的 GET 請求、匿名回覆、對成千上萬用戶看起來一樣的內容。商品清單、行銷頁面、公開個人資料頁面、未個人化的搜尋結果、OG 圖片終點、sitemap.xml。除非你對用戶 ID 使用緩存鍵,否則任何根據用戶個人化都是錯誤的。

這裡不放什麼:任何帶有Set-Cookie 在回應中,任何經過驗證的內容,任何具有寫入副作用的內容。如果你的CDN在/api/*上的命中率超過5%,你已經比大多數團隊做得更好了.

第2層:應用程式快取(Redis)

當人們說"快取"時,他們指的是應用程式快取。它位於您的服務行程中,或與服務行程並列的 Redis 中。它捕捉 CDN 不能處理的請求(因為它們需要驗證或個人化),並且無需透過資料庫迴圈即可提供這些請求。

容易部署的模式:

import json
import redis
from typing import Optional

r = redis.Redis(host="cache.internal", port=6379, decode_responses=True)

def get_user_profile(user_id: str) -> dict:
    key = f"user:profile:{user_id}"

    # try cache first
    cached = r.get(key)
    if cached is not None:
        return json.loads(cached)

    # cache miss - hit the DB
    profile = db.fetch_one(
        "SELECT id, name, plan, created_at FROM users WHERE id = $1",
        user_id,
    )

    # set with a TTL so stale data times out even if invalidation fails
    r.setex(key, 300, json.dumps(profile, default=str))
    return profile

def invalidate_user_profile(user_id: str) -> None:
    # called from any writer that mutates the user row
    r.delete(f"user:profile:{user_id}")

進入全螢幕模式 離開全螢幕模式

人有兩件事會忽略:TTL安全網和寫端失效。單獨TTL在寫入後最多5分鐘會泄露過期讀取。單獨失效在網絡暫時中斷時會永久泄露DEL。你需要兩者。帶子、安全帶,還有第三隻手在帶子上。

這裡放什麼:用戶數據、會話狀態、需要耗費大量資源的計算聚合,任何你不想在每次請求時重新計算但變化頻率不高的內容。80/20法則:選擇佔據80%數據庫負載的20%查詢,並優先緩存這些。

什麼不適用:任何需要強一致性(例如某人即將花費的銀行餘額)。任何寫入次數比讀取次數多的內容。任何一個過期讀取比50毫秒延遲更糟糕的情況.

第三層:資料庫快取(物化視圖)

物化視圖是團隊最常忽略存在的層。它們位於資料庫內部,預先計算出昂貴查詢的結果。資料庫將結果存儲為一個表格。讀取操作是 O(1) 的查詢,而不是七次連接和一個窗口函數。

Postgres 範例。一個按天、按賬戶的收益匯總,否則每次儀表板載入都會掃描事實表格:

CREATE MATERIALIZED VIEW account_revenue_daily AS
SELECT
    account_id,
    date_trunc('day', created_at)::date AS day,
    sum(amount_cents) AS revenue_cents,
    count(*) AS txn_count
FROM transactions
WHERE status = 'settled'
GROUP BY account_id, day;

CREATE UNIQUE INDEX ON account_revenue_daily (account_id, day);

-- refresh policy: every 10 minutes via pg_cron
-- CONCURRENTLY needs the unique index above to work
SELECT cron.schedule(
    'refresh-account-revenue',
    '*/10 * * * *',
    $$REFRESH MATERIALIZED VIEW CONCURRENTLY account_revenue_daily$$
);

進入全屏模式 退出全螢幕模式

REFRESH ... CONCURRENTLY 是沒有人會讀到的。沒有它,刷新會佔用一個 ACCESS EXCLUSIVE 鎖,阻礙讀取。有了它,刷新會寫入到影子副本,並且原子性地交換。在交換期間,你的磁碟會稍微多消耗一點;你不再阻礙你的儀表板。

這裡放什麼:聚合、跨越4+張表的聯接,任何底層數據變化慢於查詢速度的情況。按天匯總、排行榜、搜索側面選項,任何分析員會為其寫 CTE 的內容。

這裡不放什麼:需要實時結果的情況。物化視圖在刷新之間本質上就是過期的。如果用戶期望立即看到他們的操作反映,這層解決方案不適用。

這裡的陷阱是安靜的:物化視圖的年邁。一個團隊部署了一個,達到了儀表板延遲目標,然後繼續前進。六個月後底層表格的大小增加了三倍,刷新運行時間長達12分鐘,而且這個視圖比新鮮的更常過期。像審計查詢計劃一樣審計刷新持續時間。

第四層:查詢緩存(預備語句計劃緩存)

最深的層也是最難察覺的。每當您的駕駛員發送一條 SQL 語句,資料庫都必須解析它、規劃它、並執行它。前兩個步驟如果使用預備語句可以緩存。

大多數 ORMs 都做得不好,因為它們為每個查詢發送一個新的語句字符串 (WHERE id = 1 vs WHERE id = 2),這會使緩存失效。解決方法是參數綁定:

# bad - new statement every call, plan cache miss every time
def get_order_bad(order_id: int):
    return db.execute(f"SELECT * FROM orders WHERE id = {order_id}")

# good - same statement text, only parameters change, plan reused
def get_order_good(order_id: int):
    return db.execute(
        "SELECT * FROM orders WHERE id = $1",
        order_id,
    )

Enter fullscreen mode 退出全螢幕模式

在 Postgres 中,你可以看到什麼正在被快取:

-- requires pg_stat_statements extension
SELECT
    query,
    calls,
    mean_exec_time,
    rows
FROM pg_stat_statements
WHERE query LIKE 'SELECT % FROM orders WHERE id = $1'
ORDER BY calls DESC
LIMIT 10;

進入全螢幕模式 退出全螢幕模式

如果你看到相同的邏輯查詢出現好幾次,但帶有不同的字面值而不是 $1,你的 ORM 正在繞過計劃快取。修復 ORM 設定 (Eloquent 的 DB::statement vsDB::select 與繫結,GORM 的 RawWhere,等等) 之後,再調整其他任何東西。

這裡應該放什麼:熱情 OLTP 查詢。根據 ID 查找,向訂單表中插入,更新客戶最後一次登錄。每個查詢的獲益很小(可能只有一毫秒,最多兩毫秒)。每秒 50,000 個查詢的獲益之間,是兩個數據庫實例與十個之間的區別。

無法處理:具有變動形狀的查詢。如果您的 WHERE 子句根據使用者輸入變更結構,您就無法重用執行計劃,而這是沒問題的。

決策矩陣

「這些數據屬於哪一層」的快速捷徑:

數據形狀 CDN 應用程式快取 物化視圖 計劃快取
靜態資產,匿名
公開 GET,無權限驗證 可能
個人檔案,熱門
每小時分析資料匯總 可能 (TTL)
OLTP ID 查詢
強一致性讀取
即時寫入回饋

「可能」的列是團隊在設計審查中爭論的地方。誠實的答案是「先測量」。如果你的小時級匯總每分鐘被觸發200次,一個物化視圖是值得的。如果它每天只被觸發3次,一個具有30分鐘TTL的Redis緩存就很好,而物化視圖則是過度工程。

每一層的失效策略

每一層都希望有不同的失效故事

CDN. TTL 是大多數團隊的唯一現實選項。您可透過 API 清除特定的 URL,但在高請求率下,清除操作需要數秒才能傳播,您無法依賴它來確保正確性。使用短 TTL(熱端點使用 60 秒,目錄數據使用 5 分鐘),並接受數據的過期。對於長 TTL 的資產,對 URL 進行版本控制 (/static/app.a3f7b2.js),以便新版本是一個新鍵,而不是無效化。

應用程序快取。 事件驅動失效。每一條改變資料行的寫入路徑都會為該資料行的每一個緩存衍生版本呼叫 r.delete(key)。這一切運作良好,直到你有一個寫入到 users 的地方有12個,而某個人又新增了第十三個卻忘了失效。集中化它:每一個寫入都會經過一個儲存庫,該儲存庫在其提交後鉤子(post-commit hook)中觸發失效。

實現的視圖。 設定定期更新。決定您可接受的過期時間(10分鐘?1小時?),然後設定cron。為了在過期時間窗口內降低延遲,可以疊加事件觸發的REFRESH ... CONCURRENTLY,但要小心:更新本身是一個昂貴的操作,您不希望它每分鐘觸發100次。

設計緩存。 您不需要使它失效。數據庫管理它。您的任務是編寫對規劃器來說看起來相同的查詢。

惡手:跨層的驚慌

摧毀分層設計的bug:當一個熱緩存條目同時在多個層級過期時,每個並發請求會同時錯過每個層級,它們都一起衝向源頭.

一個實際版本:一個CDN緩存為/api/homepage 於 12:00:00 到期。千個並發請求錯過 CDN,觸及您的應用程式,錯過應用程式快取(其亦於 12:00:00 到期,因兩者的 TTL 均為 60 秒,且均設定於 11:59:00),觸及資料庫,所有觸發相同的物化視圖重建,而資料庫崩潰。

兩種模式可防止此問題。第一種是 singleflight:將 N 個相同的並發請求合併成一個,並廣播結果:

import (
    "context"
    "encoding/json"
    "github.com/redis/go-redis/v9"
    "golang.org/x/sync/singleflight"
)

var sf singleflight.Group

func GetHomepage(ctx context.Context, rdb *redis.Client) ([]byte, error) {
    key := "homepage:v3"

    if cached, err := rdb.Get(ctx, key).Bytes(); err == nil {
        return cached, nil
    }

    // singleflight: only the first concurrent miss recomputes;
    // every other caller waits on the same future
    result, err, _ := sf.Do(key, func() (interface{}, error) {
        fresh, err := buildHomepageFromDB(ctx)
        if err != nil {
            return nil, err
        }
        payload, _ := json.Marshal(fresh)
        // SETEX with a small jitter so 1000 keys don't expire on the same second
        rdb.SetEx(ctx, key, payload, ttlWithJitter(60))
        return payload, nil
    })
    if err != nil {
        return nil, err
    }
    return result.([]byte), nil
}

進入全螢幕模式 離開全螢幕模式

第二個是 鎖定 + 服務過期內容。在快過期時,將過期值保留在快取中,當一個請求正在重建時回傳它。虛擬模式:

def get_with_stale(key: str, fetch_fn, fresh_ttl=60, stale_ttl=600):
    payload = r.get(key)
    meta = r.get(f"{key}:meta")

    if payload and meta and meta == "fresh":
        return json.loads(payload)

    # try to grab the rebuild lock; if we get it, rebuild
    lock_key = f"{key}:lock"
    got_lock = r.set(lock_key, "1", nx=True, ex=10)

    if got_lock:
        try:
            fresh = fetch_fn()
            r.setex(key, stale_ttl, json.dumps(fresh))
            r.setex(f"{key}:meta", fresh_ttl, "fresh")
            return fresh
        finally:
            r.delete(lock_key)

    # we didn't get the lock - serve stale if we have it
    if payload:
        return json.loads(payload)

    # no stale, no lock - wait briefly and retry the read
    time.sleep(0.05)
    payload = r.get(key)
    return json.loads(payload) if payload else fetch_fn()

進入全螢幕模式 離開全螢幕模式

現在一千個並發請求觸發一次重建。其他的九百九十九個則獲得稍顯過期的值(可接受)或等待50毫秒然後獲取新寫入的值。資料庫只看到一個額外的查詢而不是一千個.

在每一層都應用這個模式。CDNs原生就這樣做。stale-while-revalidate 指令。應用程式快取需要你來設定它。實體視圖設計上就是暫存中重新驗證的。規劃快取沒有這個問題.

星期一要做什么

讓一個請求通過你的系統,標示每個數據片段應該存放在哪個層級。大多數團隊發現他們有一個層級在做三個層級的工作,或者三個層級都在快取同一件事,但有不同的TTL且不一致。

選擇錯誤率與成本比最高的那層。加入它。測量。重複。目標不是使用每一層。而是將每一個資料放在回答其特定問題的那一層,然後確保它們不會爭相前進.

你目前的系統中,哪一層負擔最多工作,哪一層又默默缺席?


如果這有幫助

四層快取堆疊是系統設計面試中會問到的類型,也是你的運營團隊會感謝你發布的類型。系統設計口袋指南:基礎 涵蓋了緩存,收錄在「效能基礎原語」章節中,與負載平衡、佇列和複製模式並列,這些模式決定了你的設計能否在規模上穩定運行。如果這篇文章對你有幫助,那本書在250多頁的建構基礎中,使用了相同的語氣。

System Design Pocket Guide: Fundamentals — Core Building Blocks for Scalable Systems