慣性聚合 関心のあるブログ、ニュース、テクノロジーを効率的に追跡
原文を読む 慣性聚合で開く

おすすめ購読元

博客园 - 司徒正美
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、アプリ、DB、クエリ:どこに何があるか
Gabriel Anha · 2026-05-24 · via DEV Community

ユーザーのリクエストと、それが参照している行の間には、4つのキャッシュレイヤーが存在します。多くのチームは2つを使用します(通常はDBの前にRedisを使用し、静的アセットにはCDNを使用)そして残りの2つは他の誰かの問題として扱います。

その結果、CDNが行うべき仕事をRedisの階層が行い、データベースが静かにそのプランキャッシュを品質のクッションとして使用し、そして毎回のホットキーの有効期限切れごとに大混乱が起きることになります。各レイヤーは異なる質問に答えます。間違ったレイヤーを選んでしまうと、選ばなかったレイヤーの代償を払うことになります。

キャッシングがレイヤー化される理由

パターンが重要なのは、質問が積み重なるからです。

  • 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)ことを可能にします。

ここに入るもの:publicなGETリクエスト、匿名のレスポンス、数千のユーザーにとって同じように見えるもの。商品リスト、マーケティングページ、公開プロフィールページ、パーソナライズなしの検索結果、OGイメージエンドポイント、sitemap.xml。ユーザーごとにパーソナライズするものはここでは間違っており、ユーザーIDにキャッシュキーを設定しない限りはダメです.

ここにはないもの:ユーザーIDに関連するものSet-Cookie に記載されている内容、認証されているもの、書き込み副作用があるもの。/api/* の CDNヒット率が5%を超えていれば、既に大多数のチームよりも良い成果を上げていることになります。

レイヤー2:アプリケーションキャッシュ(Redis)

アプリケーションキャッシュとは、「キャッシュ」と言うときに人々が意味するものです。サービスプロセスの中に、あるいはその隣のRedisにあります。CDNが処理できなかったリクエスト(認証されたり、パーソナライズされたりするため)をキャッチし、DBのラウンドトリップなしでそれを提供します.

うまく動作するパターン:

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のルール:DBの負荷の20%を占めるクエリの20%を選び、それを最初にキャッシュする。

何でもない:強い一貫性が必要なもの(すぐに使おうとしている銀行口座の残高など)。読むよりも書き込むことが多いもの。古くなった読み出しの方が50msの遅延より悪いもの.

レイヤー3:データベースキャッシュ(実装ビュー)

物化ビューは最もチームが存在しないと忘れがちな層です。それらはデータベースの内部にあり、高コストなクエリの結果を事前に計算しています。データベースは結果をテーブルとして格納します。読み取りは7つのジョインとウィンドウ関数ではなく、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を書くであろうもの。

ここに入らないもの:リアルタイムで結果が必要なもの。マテリアライズされたビューはリフレッシュの間常に古くなっている。ユーザーが自分のアクションがすぐに反映されることを期待している場合、このレイヤーは答えではない。

ここでの落とし穴は静かです:物化されたビューの年齢化です。チームが一つをリリースし、ダッシュボードのレイテンシ目標を達成し、進みます。6ヶ月後、下位のテーブルのサイズは3倍になり、リフレッシュは12分かかり、ビューは新鮮なものより stale であることが多いです。クエリプランの監査のようにリフレッシュ期間を監査してください.

レイヤー4:クエリキャッシュ(準備されたステートメントプランキャッシュ)

最も深い層はまた最も見えにくいものです。あなたのドライバーがSQL文を送るたびに、データベースはそれをパースし、プランニングし、実行しなければなりません。最初の2つのステップは、プリペアドステートメントを使用すればキャッシュできます.

ほとんどのORMはこれをうまく行いません。なぜなら、毎回クエリのために新しいステートメント文字列を送るからです(WHERE id = 1 ではなく 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の対)DB::select にはバインディングが付いており、GORMの RawWhere などが存在します。他の何かをチューニングする前に確認してください。

ここに何が入りますか:ホットなOLTPクエリです。IDによる検索、注文への挿入、顧客の最終アクセス時刻の更新。1クエリあたりの利点は小さい(ミリ秒1つか2つ程度)です。50,000クエリ/秒あたりの利点は、2つのデータベースインスタンスと10つのインスタンスの違いです。

変数の形状を持つクエリは機能しない。WHERE句がユーザー入力に基づいて構造を変更する場合、プランを再利用できず、それで問題はない。

決定基準

「このデータはどの層に属しているか」のショートカット:

データ形状 CDN アプリケーションキャッシュ 物化ビュー プランキャッシュ
静的資産、匿名 はい いいえ いいえ いいえ
公開GET、認証不要 はい 未定 いいえ いいえ
ユーザーごとプロファイル、人気 いいえ はい いいえ はい
時間ごとの分析結果のまとめ いいえ 未定(TTL) はい いいえ
OLTP ID の検索 いいえ はい いいえ はい
強い一貫性のある読み取り いいえ いいえ いいえ はい
リアルタイム書き込みフィードバック いいえ いいえ いいえ はい

「かもしれない」行はデザインレビューでチームが議論する場所です。正直な答えは「まず測定する」です。あなたの時間ごとの集計が1分間に200回ヒットする場合、マテリアライズされたビューはその価値を証明します。もし1日3回ヒットする場合、30分のTTLを持つRedisキャッシュで十分で、マテリアライズされたビューは過剰設計です。

各レイヤーごとの無効化戦略

各レイヤーは異なる無効化の物語を望む

CDN TTLは大多数のチームにとって唯一現実的な選択肢です。特定のURLをAPI経由でクリーンアップできるが、高いリクエストレートではクリーンアップが数秒かけて propagate され、正確性に頼ることができません。短い TTL を使用してください(ホットエンドポイントには60秒、カタログデータには5分)。古いデータを受け入れます。長い TTL の資産については、URL をバージョン化する (/static/app.a3f7b2.js) ことで、新しいバージョンが新しいキーになるようにし、無効化されないようにします.

アプリケーションキャッシュ。 イベント駆動型の無効化。行を変更するすべての書き込みパスは、その行のすべてのキャッシュされた派生に対してr.delete(key)を呼び出します。これが機能するのは、usersに書き込みがある12箇所がある場合までです。13番目を追加した人が無効化を忘れると、問題が発生します。中央集権化しましょう:すべての書き込みは、コミット後のフックの一部として無効化を発火するリポジトリを通じて行われます。

マテリアライズされたビュー。 定期的リフレッシュをスケジュールします。あなたの受け入れ可能な古さ(10分?1時間?)を決定し、cronを設定します。古さのウィンドウでの低い遅延のためには、イベント駆動のREFRESH ... CONCURRENTLYトリガーを層化しますが、注意してください:リフレッシュ自体が高コストの操作であり、1分に100回発火したくありません.

キャッシュの計画. あなたはそれを無効にしません。データベースがそれを管理します。あなたの仕事は、プランナーにとって同じように見えるクエリを書くことです。

陷阱:各层数据の暴走

層状設計を破壊するバグ:あるヒートキャッシュエントリーが複数の層で同時に有効期限切れになった場合、すべての並行リクエストが各層を同時に見つからず、それらはすべてオリジンに一斉に暴走する.

実際の例:CDNキャッシュ/api/homepage は 12:00:00 に有効期限切れになります。1000件の同時リクエストがCDNをミスし、あなたのアプリケーションをヒットし、アプリケーションキャッシュをミスします(これも有効期限切れです:両方のTTLが60秒で、両方とも11:59:00に設定されていたため)。データベースをヒットし、すべてが同じ物化ビューの再構築をトリガーし、データベースがクラッシュします。

この問題を防ぐ2つのパターンがあります。最初は singleflight です:N個の同じ同時リクエストを1つにまとめ、結果をブロードキャストします。

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()

フルスクリーンモードを開始 フルスクリーンモードを終了

今や千件の並行リクエストが一つの再構築を引き起こす。他の999件は少し古い値(受け入れ可能)を受け取るか、50ms待って新しく書かれたものを拾う。データベースは千件のクエリの代わりに一つの追加クエリを見る.

このパターンを各レイヤーで適用しよう。CDNはネイティブにそれをやっている。stale-while-revalidate の指示。アプリケーションキャッシュはあなたに接続する必要があります。マテリアライズドビューは設計上、再検証中に古いという性質があります。プランキャッシュはその問題を持ちません.

月曜日の予定

システムを通じてリクエストをたどり、各データがどのレイヤーに存在すべきかラベル付けします。多くのチームは、1つのレイヤーが3つの仕事をしていることに気づき、または同じことをキャッシュしている3つのレイヤーがあり、異なるTTLで合意しないことに気づきます。

最もミスコスト比が高いレイヤーを選択し、追加して測定し、繰り返します。目的はすべてのレイヤーを使用することではありません。各データが特定の質問に答えるレイヤーに配置し、それらが競争しないようにすることです。

現在のシステムで最も多くの作業を行っているレイヤーはどれか、静かに欠落しているレイヤーはどれか?


これは役立った면

四層キャッシュスタックはシステム設計の面接で聞かれるようなものであり、オペレーションチームが配信してくれたことを感謝するようなものです。System Design Pocket Guide: Fundamentals は、「パフォーマンスプリミティブ」という章でキャッシングについてカバーしており、ロードバランシング、キューイング、およびリプレーション パターンと共に、あなたの設計がスケールで機能するかどうかを決定するものです。この投稿が役立ったなら、本は250ページ以上の構築ブロック全体で同じ声を保っています。

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