はじめに
プロファイリングでボトルネックを測定することでも、負荷テストでシステム全体を評価することでもない。「DBへのアクセスをキャッシュで減らす」——Cache-AsideかWrite-Throughか、TTLを何秒に設定するか、キャッシュが古くなったらどう無効化するか、という設計判断をClaude Codeで実装する話だ。
「キャッシュ無効化はコンピュータサイエンスで最も難しい問題の一つだ」——そういう格言がある。キャッシュ自体は単純なget/setに見えるが、「いつ古くなるか」「どうやって削除するか」「分散環境でどう伝播させるか」という問題が積み重なると、後付けで追加したキャッシュが本番でメモリを食い続け、古いデータを返し続けるという状況に陥る。
CLAUDE.mdでキャッシュポリシーを定義し、Upstash MCP Serverを導入することで、Claude Codeはこれらの設計判断を毎回ゼロから考えずに済むようになる。
CLAUDE.mdでキャッシュポリシーを定義する——TTL規約・パターン選択・禁止事項
「TTLなしのキャッシュ」という時限爆弾
Redisを使い始めたチームが最初に踏む落とし穴は、TTLなしのキャッシュだ。SETPEXを使わずSETだけで書いたキャッシュは永久に残り続ける。商品情報が更新されても古い値が返され続け、Redisのメモリが徐々に圧迫されていく。「パフォーマンス改善のために入れたキャッシュが本番障害の原因になった」という話は珍しくない。
CLAUDE.mdにキャッシュポリシーを定義することで、Claude Codeは実装のたびにこの落とし穴を回避できるようになる。
# CLAUDE.md(Redisキャッシュポリシー)
## Cache Policy
### 基本ルール
- 全キャッシュにTTLを必須設定(無期限キャッシュ禁止)
- キャッシュキーには必ずプレフィックスを付ける: {service}:{resource}:{id}
例: user:profile:123、product:catalog:456
- Nullキャッシュを実装すること(DBミス時も短期TTLで結果をキャッシュ)
### TTL規約
- マスターデータ(商品・カテゴリ): 3600秒(1時間)
- ユーザープロフィール・セッション: 900秒(15分)
- 集計・ダッシュボード: 300秒(5分)
- レート制限カウンター: 60秒
- Nullキャッシュ(存在しないリソース): 60秒
### パターン選択規約
- 読み取り多・書き込み少(商品検索・ユーザープロフィール): Cache-Aside
- 強一貫性が必要(残高・在庫): Write-Through
- ホットキー(Top100ランキング等): ローカルメモリキャッシュ(20〜30秒)+ Redis(120秒)の二重化
### 禁止パターン
- TTLなしのSET(永久キャッシュ)
- キャッシュキーへのユーザー入力の直接埋め込み(インジェクション対策)
- トランザクション内でのキャッシュ更新(DB commit前にキャッシュを書かない)このCLAUDE.mdがある状態でClaude Codeに「このAPIにキャッシュを追加して」と依頼すると、TTL設定・プレフィックス付きのキー構成・Nullキャッシュ実装が自動的に生成される。なぜ禁止パターンとして「トランザクション内でのキャッシュ更新」を明記しているかは次のセクションで詳しく説明する。
TTL規約の設計ロジック
TTLの設定は感覚で決めると後で問題が起きる。「短すぎるとDBへの負荷が減らず、長すぎると古いデータを返す」というトレードオフを、データの種別ごとに整理しておく必要がある。
商品カタログのようなマスターデータは更新頻度が低く、1時間程度古くても業務に支障がない場合が多い。ユーザープロフィールは15分程度で十分で、セッション連動で管理できる。集計データやダッシュボード数値は5分程度のキャッシュが多くの場合に妥当で、リアルタイム性より処理負荷軽減を優先する。
これをCLAUDE.mdに明記しておくことで、Claude Codeが新しいエンドポイントにキャッシュを追加する際、毎回「このデータのTTLは何秒か」を考えずに済む。
Cache-Aside / Write-Throughパターンの実装——Claude Codeが生成するキャッシュコード
Cache-Aside:読み取り多のデータに最適
Cache-Asideは最もシンプルかつ一般的なキャッシュパターンだ。アプリケーションがキャッシュとDBを直接制御し、「キャッシュにあれば使う、なければDBから取る」という流れになる。
Claude Codeへの指示はこうなる。
このAPIエンドポイントにRedisキャッシュを追加して。
対象: GET /products/{id}(商品詳細)
要件:
- Cache-Asideパターンで実装
- TTL: 1時間(商品データは更新頻度低い)
- キャッシュキー: product:detail:{id}
- Nullキャッシュ: 存在しない商品IDは60秒キャッシュ
- 商品更新API(PUT /products/{id})でキャッシュを無効化
- CLAUDE.mdのキャッシュ規約に準拠してClaude Codeが生成する実装はこうなる。
async function getProduct(productId: string) {
const cacheKey = `product:detail:${productId}`;
// 1. キャッシュを確認
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
// 2. キャッシュミス: DBから取得
const product = await db.products.findById(productId);
if (!product) {
// Nullキャッシュ(存在しない商品IDの連続DBアクセスを防ぐ)
await redis.setex(`${cacheKey}:null`, 60, '1');
return null;
}
// 3. キャッシュに書き込み(TTL 1時間)
await redis.setex(cacheKey, 3600, JSON.stringify(product));
return product;
}
// 商品更新時のキャッシュ無効化
async function updateProduct(productId: string, data: Partial<Product>) {
await db.products.update(productId, data);
// DBコミット後にキャッシュを削除(次の読み取りで再構築される)
await redis.del(`product:detail:${productId}`);
}Nullキャッシュは見落とされがちな実装だが重要だ。存在しないIDへのリクエストが大量に来た場合、キャッシュミスのたびにDBへのクエリが走る「キャッシュスタンピード」が発生する。Nullキャッシュを入れることで、存在しないリソースへのアクセスもキャッシュが吸収できるようになる。
Write-Through:強一貫性が必要なデータの設計
残高・在庫・決済状態など、古いデータを返すことが許容されないデータにはWrite-Throughパターンが適している。データを更新する際に、DBとキャッシュを同時に更新する。
ここで重要な実装上の注意がある。DBトランザクション内でキャッシュを書いてはいけない。
// Write-Through: DBコミット後にキャッシュを更新(順序が重要)
async function updateInventory(productId: string, quantity: number) {
// 1. まずDBトランザクションを完了させる
await db.transaction(async (tx) => {
await tx.inventory.update(productId, { quantity });
// ここではキャッシュを書かない!
// トランザクションがロールバックした場合にキャッシュだけ更新されて
// DBと不整合な状態になるため
});
// 2. DBコミット成功後にキャッシュを更新(トランザクション外)
// この時点でロールバックリスクはないため安全にキャッシュを書ける
await redis.setex(`product:inventory:${productId}`, 3600, String(quantity));
}CLAUDE.mdの禁止パターンに「トランザクション内でのキャッシュ更新」を明記した理由はここにある。db.transactionのコールバック内でredis.setexを呼ぶと、DBのロールバックが発生したときにキャッシュだけが更新された状態になる。以降のリクエストはキャッシュから古いデータを取得し続け、DB側の正しいデータと不整合が生まれる。
DBコミット後・Redisセット前のタイミングでサーバーがクラッシュした場合は、次回の読み取りでキャッシュミスが発生してDBから最新値が取得されるため、結果的に整合性が回復する。
Upstash MCP Server × キャッシュ無効化——Redisを自然言語で操作・デバッグ
Upstash公式MCPサーバーの設定
Upstashが提供する公式MCPサーバー(github.com/upstash/mcp-server)をClaude Codeに追加することで、RedisデータベースをClaude Codeから自然言語で直接操作できる。
claude mcp add upstash -- npx -y @upstash/mcp-server@latest \
--email YOUR_EMAIL --api-key YOUR_API_KEYこのMCPサーバーが提供する主な機能は、Redisデータベースの作成・削除・バックアップ、Redisコマンドの自然言語実行、リアルタイムのDB使用量・スループット・レイテンシ確認だ。
なお、Upstashはマネージドサービスなので、ElastiCacheやセルフホストのRedisを使っている場合はMCPサーバーの設定が異なる。上記のセットアップはUpstash環境向けのものと理解した上で、既存Redis環境への適用は公式ドキュメントを確認してほしい。
本番キャッシュのデバッグを安全に行う
Upstash MCPを使ったデバッグのフローは実際のプロジェクトで効果を発揮する。
「本番Redisの user:profile:* キーのTTL設定を全て確認して。
TTLなしのキーがあれば教えて」
↓ MCP経由でRedisをスキャン・TTL確認
「過去24時間でキャッシュヒット率が最も低いキーパターンを教えて」
↓ メトリクスを分析・改善提案
「ステージング環境のキャッシュを全てクリアして。パターン: staging:*」
↓ 安全にキャッシュを削除直接Redisコマンドを実行する場合、KEYS user:profile:*のようなコマンドのパターンミスで意図しないキーを削除してしまうリスクがある。Claude Code経由で自然言語指示にすることで、削除対象をClaude Codeが解釈してから実行するため、このリスクを軽減できる。
Pub/Subによるキャッシュ無効化の伝播
分散環境ではキャッシュ無効化をどう伝播させるかが重要な課題になる。単一サービスであれば更新時にキャッシュを削除するだけで済むが、複数のサービスが同じデータをキャッシュしている場合、一方のサービスがデータを更新しても他のサービスのキャッシュは古いままになる。
Pub/Subを使った無効化イベントの伝播がこの問題の解決策になる。
// 商品更新時にPub/Subで無効化イベントを発行
async function updateProduct(productId: string, data: Partial<Product>) {
await db.products.update(productId, data);
await redis.del(`product:detail:${productId}`);
// 他サービスのローカルキャッシュも含めて無効化を通知
await redis.publish('cache:invalidate:product', productId);
}
// 全サービスがsubscribeして自分のローカルキャッシュも無効化
redis.subscribe('cache:invalidate:product', (productId) => {
localCache.delete(`product:${productId}`);
});TTL切れを待たずに全サービスのキャッシュを即座に無効化できるため、商品価格の更新が全サービスにリアルタイムで反映される。
まとめ
Redisキャッシュが「後付けで問題を起こす」原因の大半は、TTL設定の漏れ・パターン選択の曖昧さ・トランザクション内でのキャッシュ更新という3つの設計ミスに集約される。これらをCLAUDE.mdの禁止パターンと規約として明文化することで、Claude Codeが実装するたびに同じミスが繰り返されなくなる。
取り組むとしたら、まず既存のRedisキャッシュコードをClaude Codeにレビューさせることから始めるのが現実的だ。「このコードのキャッシュ実装でTTLなし・トランザクション内更新・Nullキャッシュなしの箇所を全て特定して」という指示で、潜在的な問題が一覧で出てくる。それをCLAUDE.mdのポリシーと照合することで、どの規約が守られていないかが可視化される。
Upstash MCPは特に「本番での調査」で効果を発揮する。TTLなしキーの確認やキャッシュヒット率の分析が自然言語でできるようになれば、Redisの専門知識が深くないエンジニアも本番キャッシュの状態を把握しやすくなる。

コメント