キャッシュが必要な理由
データベースへのクエリは、どれほど最適化しても本質的にI/Oを伴う処理です。高トラフィックのアプリケーションでは、同じデータへのリクエストが秒間数千回発生することもあります。Redisのようなインメモリキャッシュはデータをメモリに保持するため、マイクロ秒単位の応答が可能です。適切なキャッシュ戦略を導入することで、データベースの負荷を大幅に削減しながらレスポンスタイムを劇的に改善できます。
パターン1:Cache-Aside(ラルーキャッシュ)
最も一般的なパターンです。アプリケーションがキャッシュを管理し、キャッシュミス時にデータベースから取得してキャッシュに格納します。
-- PostgreSQL: 頻繁にアクセスされるユーザープロフィール
SELECT id, name, email, avatar_url, subscription_tier
FROM users
WHERE id = $1;
-- Redisキャッシュキー設計
-- user:{userId}:profile → JSONで保存
-- TTL: 3600秒(1時間)
-- 擬似コード(TypeScript風)
-- async function getUserProfile(userId: string) {
-- const cacheKey = `user:${userId}:profile`;
--
-- // 1. キャッシュを確認
-- const cached = await redis.get(cacheKey);
-- if (cached) return JSON.parse(cached);
--
-- // 2. DBから取得
-- const user = await db.query(
-- 'SELECT id, name, email FROM users WHERE id = $1',
-- [userId]
-- );
--
-- // 3. キャッシュに保存(TTL付き)
-- await redis.setex(cacheKey, 3600, JSON.stringify(user));
-- return user;
-- }
パターン2:Write-Through(書き込み時更新)
データ更新時にキャッシュとデータベースを同時に更新します。キャッシュが常に最新の状態に保たれますが、書き込みコストが増加します。
-- 商品在庫の更新(在庫は常に最新値が必要)
UPDATE products SET stock_count = stock_count - $1
WHERE id = $2 AND stock_count >= $1
RETURNING id, stock_count;
-- 同時にRedisも更新
-- HSET product:${productId} stock_count ${newCount}
-- 在庫確認クエリ(キャッシュから取得)
-- HGET product:{productId} stock_count
-- セッション管理への応用
-- ユーザーセッションはWrite-Throughで管理
-- DBとRedis両方にセッション情報を保持
パターン3:Read-Through(読み取り時自動ロード)
Cache-Asideに似ていますが、キャッシュミス時の処理をキャッシュレイヤー自体が担います。アプリケーションコードが簡潔になります。
-- 集計データのキャッシュ(再計算コストが高い)
-- キャッシュキー: stats:monthly:{year}:{month}
-- TTL: 3600秒(更新頻度が低いため長め)
SELECT
COUNT(*) AS total_users,
COUNT(*) FILTER (WHERE subscription_tier = 'pro') AS pro_users,
SUM(monthly_revenue) AS total_revenue
FROM (
SELECT
u.id,
u.subscription_tier,
COALESCE(SUM(o.total_amount), 0) AS monthly_revenue
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
AND DATE_TRUNC('month', o.created_at) = DATE_TRUNC('month', NOW())
GROUP BY u.id, u.subscription_tier
) AS user_stats;
キャッシュ設計の重要な考慮事項
キャッシュキーの設計
キャッシュキーは一意性と可読性を両立させる必要があります。
-- キャッシュキー設計の例
-- {entity_type}:{id}:{scope}
-- user:123:profile → ユーザープロフィール
-- user:123:orders:page:1 → ユーザーの注文一覧(1ページ目)
-- product:456:details → 商品詳細
-- stats:daily:2026-04-10 → 日次統計
-- キャッシュ無効化のパターン
-- ユーザー情報が変わったら関連キャッシュを全削除
-- DEL user:123:profile
-- DEL user:123:orders:page:* (パターンマッチで削除)
キャッシュスタンピード(Thundering Herd)対策
人気コンテンツのキャッシュが失効した瞬間に大量のリクエストがDBに集中する問題です。
-- 対策1: Probabilistic Early Expiration
-- TTLの残り時間が少なくなったら確率的に早めに再生成
-- 対策2: ロック付きキャッシュ再生成
-- SET lock:user:123:profile 1 NX EX 5 (排他ロック)
-- ロックが取れたスレッドだけがDBから取得・更新
-- 他のスレッドはロック解放を待ってキャッシュを読む
-- 対策3: バックグラウンドリフレッシュ
-- キャッシュ失効前に非同期でデータを更新
-- TTLが閾値以下になったらジョブをキューに追加