キャッシング戦略 - パフォーマンス最適化の基礎

14分 で読める | 2025.12.26

公式ドキュメント

キャッシュとは

キャッシュは、データのコピーを高速にアクセスできる場所に一時的に保存する仕組みです。元のデータソース(データベース、API等)へのアクセスを減らし、レスポンス時間を短縮します。

キャッシュの効果: データベースクエリが100msかかる場合、キャッシュからの取得は1ms未満で完了することもあります。

キャッシュの層

  1. ブラウザキャッシュ
  2. CDNキャッシュ
  3. アプリケーションキャッシュ(Redis等)
  4. データベースキャッシュ
  5. データベース

キャッシングパターン

Cache-Aside(キャッシュアサイド)

アプリケーションがキャッシュとデータベースを直接管理します。

async function getUser(userId) {
 // 1. キャッシュを確認
 const cached = await cache.get(`user:${userId}`);
 if (cached) {
 return JSON.parse(cached);
 }

 // 2. キャッシュミス: DBから取得
 const user = await db.users.findById(userId);

 // 3. キャッシュに保存
 await cache.setex(`user:${userId}`, 3600, JSON.stringify(user));

 return user;
}

メリット: シンプル、障害に強い デメリット: キャッシュミス時のレイテンシ

Read-Through(リードスルー)

キャッシュ自体がデータの取得を担当します。

// キャッシュライブラリの設定
const cache = new Cache({
 loader: async (key) => {
 // キャッシュミス時に自動的に呼ばれる
 const userId = key.replace('user:', '');
 return await db.users.findById(userId);
 }
});

// 使用(シンプル!)
const user = await cache.get(`user:${userId}`);

Write-Through(ライトスルー)

書き込み時にキャッシュとDBを同時に更新します。

async function updateUser(userId, data) {
 // DBを更新
 const user = await db.users.update(userId, data);

 // キャッシュも同時に更新
 await cache.setex(`user:${userId}`, 3600, JSON.stringify(user));

 return user;
}

メリット: データの一貫性が高い デメリット: 書き込みレイテンシが増加

Write-Behind(ライトビハインド)

注意: Write-Behindは高速ですが、キャッシュ障害時にデータ損失のリスクがあります。重要なデータには使用を避けてください。

キャッシュに即座に書き込み、DBへの反映は非同期で行います。

async function updateUser(userId, data) {
 // キャッシュを即座に更新
 await cache.setex(`user:${userId}`, 3600, JSON.stringify(data));

 // DBへの書き込みはキューに追加
 await writeQueue.add({ userId, data });

 return data;
}

// バックグラウンドワーカー
writeQueue.process(async (job) => {
 await db.users.update(job.userId, job.data);
});

メリット: 高速な書き込み デメリット: データ損失のリスク

キャッシュ無効化

TTL(Time To Live)

一定時間後に自動的に期限切れにします。

// 60秒後に期限切れ
await cache.setex('key', 60, 'value');

イベントベースの無効化

データ更新時に明示的にキャッシュを削除します。

async function updateUser(userId, data) {
 await db.users.update(userId, data);

 // 関連するキャッシュを無効化
 await cache.del(`user:${userId}`);
 await cache.del(`user:${userId}:profile`);
 await cache.del(`users:list`);
}

パターンベースの無効化

// ユーザー関連のすべてのキャッシュを削除
const keys = await cache.keys('user:123:*');
await cache.del(...keys);

キャッシュの問題と対策

キャッシュスタンピード(雪崩)

多数のリクエストが同時にキャッシュミスを起こす問題です。

// 対策: ロックを使用
async function getWithLock(key, loader) {
 const cached = await cache.get(key);
 if (cached) return JSON.parse(cached);

 // ロックを取得
 const lockKey = `lock:${key}`;
 const locked = await cache.set(lockKey, '1', 'NX', 'EX', 10);

 if (!locked) {
 // 他のプロセスがロード中 → 少し待ってリトライ
 await sleep(100);
 return getWithLock(key, loader);
 }

 try {
 const data = await loader();
 await cache.setex(key, 3600, JSON.stringify(data));
 return data;
 } finally {
 await cache.del(lockKey);
 }
}

確率的早期再計算

TTL切れ前に確率的にキャッシュを更新します。

async function getWithProbabilisticRefresh(key, loader, ttl) {
 const data = await cache.get(key);
 const remainingTtl = await cache.ttl(key);

 // TTLが残り少ない場合、確率的に再計算
 if (data && remainingTtl < ttl * 0.1) {
 if (Math.random() < 0.1) {
 // 10%の確率でバックグラウンド更新
 loader().then(newData => {
 cache.setex(key, ttl, JSON.stringify(newData));
 });
 }
 }

 if (data) return JSON.parse(data);

 const newData = await loader();
 await cache.setex(key, ttl, JSON.stringify(newData));
 return newData;
}

実践メモ: キャッシュキーにはプレフィックス:ID:サブリソース:バージョンの構造を使い、バージョンを含めることでスキーマ変更時の互換性を保てます。

キャッシュキーの設計

// 良いキー設計
const key = `user:${userId}:profile:v2`;

// 構成要素:
// - プレフィックス: エンティティタイプ
// - 識別子: ユニークなID
// - サブリソース: 具体的なデータ
// - バージョン: スキーマ変更時の互換性

ポイント: TTLはデータの更新頻度に応じて設計します。静的コンテンツは長く、リアルタイムデータは短く設定するのが基本です。

TTLの設計指針

データタイプTTL理由
静的コンテンツ1日〜1週間ほとんど変更されない
ユーザープロファイル1〜24時間変更頻度が低い
設定情報5〜30分適度に更新される
リアルタイムデータ1〜5分頻繁に変更される
セッション30分〜24時間セキュリティとUXのバランス

まとめ

キャッシングは、パフォーマンス最適化の基本的な手法です。Cache-Aside、Write-Through等のパターンを理解し、適切なTTLと無効化戦略を設計することで、高速でスケーラブルなシステムを構築できます。キャッシュの複雑さとメリットのバランスを考慮して導入しましょう。

参考リソース

← 一覧に戻る
PR
PR
PR
PR