キャッシュとは
キャッシュは、データのコピーを高速にアクセスできる場所に一時的に保存する仕組みです。元のデータソース(データベース、API等)へのアクセスを減らし、レスポンス時間を短縮します。
キャッシュの効果: データベースクエリが100msかかる場合、キャッシュからの取得は1ms未満で完了することもあります。
キャッシュの層
ブラウザキャッシュ
↓
CDNキャッシュ
↓
アプリケーションキャッシュ(Redis等)
↓
データベースキャッシュ
↓
データベース
キャッシングパターン
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(ライトビハインド)
キャッシュに即座に書き込み、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;
}
キャッシュキーの設計
// 良いキー設計
const key = `user:${userId}:profile:v2`;
// 構成要素:
// - プレフィックス: エンティティタイプ
// - 識別子: ユニークなID
// - サブリソース: 具体的なデータ
// - バージョン: スキーマ変更時の互換性
TTLの設計指針
| データタイプ | TTL | 理由 |
|---|---|---|
| 静的コンテンツ | 1日〜1週間 | ほとんど変更されない |
| ユーザープロファイル | 1〜24時間 | 変更頻度が低い |
| 設定情報 | 5〜30分 | 適度に更新される |
| リアルタイムデータ | 1〜5分 | 頻繁に変更される |
| セッション | 30分〜24時間 | セキュリティとUXのバランス |
まとめ
キャッシングは、パフォーマンス最適化の基本的な手法です。Cache-Aside、Write-Through等のパターンを理解し、適切なTTLと無効化戦略を設計することで、高速でスケーラブルなシステムを構築できます。キャッシュの複雑さとメリットのバランスを考慮して導入しましょう。
← 一覧に戻る