要点: 公開データは利用者の近くへ置けますが、非公開データは必ずテナント単位で分離し、更新ルールを明示する必要があります。
この記事の用語: テナント:同じ基盤を利用する一つの顧客。Invalidation:古いキャッシュを無効にする処理。
現場の課題
Redisを追加するだけでは安全なキャッシュにはなりません。キーの失効時に同じクエリがDBへ集中し、設定を一つ誤れば別テナントのデータを返す危険もあります。
キャッシュは性能改善であると同時に、別の場所へ置くデータの複製です。誰のデータか、どこまで古くてよいか、何を契機に更新するかを先に決めなければなりません。
設計と実装
- 保存先を決める前に、レスポンスを公開情報と非公開情報へ分類します。
- Single-flightを使い、キー失効時にキャッシュを再構築するリクエストを一つだけに制限します。
- キャッシュキーへテナントIDとデータバージョンを含め、関連データの変更時にバージョンを進めます。
実装例
key = `catalog:${tenantId}:v${catalogVersion}:${queryHash}`
cached = await redis.get(key)
if (cached && cached.age < freshFor) return cached.value
lock = await redis.set(`${key}:lock`, requestId, { NX: true, PX: 3000 })
if (!lock && cached && cached.age < staleFor) return cached.value
value = await db.catalog.findMany({ where: { tenantId } })
await redis.set(key, encode(value), { EX: staleFor })
return value本番導入の進め方
まず読み取りが多くリスクの低いエンドポイントを一つ選び、比較だけを行う比較モードでヒット率、データの古さ、Redis遅延、DBクエリ数を測ります。結果が安定してから少数テナントへ有効化します。
まとめ
良いキャッシュ設計は速いだけでなく、データの所有者、有効期間、更新条件を誰でも説明できます。
