cache-strategy について
恒久的なキャッシュ優先方式を適用する開発スキルです。読み取り時はキャッシュを参照し、未ヒットならデータベースを読み、データ変更後に無効化する手順を扱います。対象選定、キー規約、任意の事前ウォームアップも含みます。
主な機能
- 01既存のキャッシュ基盤を調査
- 02キャッシュ対象のデータベース読み取りを特定
- 03キャッシュ優先読み取りを実装
- 04書き込みや削除後にキャッシュを無効化
- 05キャッシュキーの命名規則を定義
- 06任意の起動時ウォームアップを追加
活用シーン
- 01頻繁に参照され変更の少ないデータのDB読み取りを削減
- 02ユーザープロフィールや商品詳細をキャッシュ
- 03変更後に関連エントリーや一覧を削除
- 04重要データをアプリ起動時に事前ウォームアップ
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ ariadoss/ superskills --skill cache-strategyインストール後、Claude Code で/cache-strategyを入力して読み込まれたか確認します。
詳細
/cache-strategy
永続的なキャッシュファースト戦略を実装します。まずキャッシュから読み込み、キャッシュミス時には DB にフォールバックし、キャッシュに書き込みます。無効化するのはデータが変更されたときだけです。期限切れタイマーは使用しません。キャッシュは、基盤となるデータが変更されるまで有効です。
考え方
目標は、(データが変更されるまで)データ項目ごとの DB 読み取りを生涯で 1 回にすることです。
READ: cache hit? → return cached value
cache miss? → query DB → write to cache → return value
WRITE: update DB → invalidate (or update) cache entry → done
これは Play Framework のモデルです。TTL、ポーリング、バックグラウンド更新はありません。ソースが変更されたときに正確に無効化されるため、キャッシュは常にウォームで、常に正確です。
TTL より永続キャッシュを選ぶ理由
- TTL キャッシュはシンプルさと引き換えに古いデータの読み取りを許容します。データが何も変更されていなくても、期限切れのたびに必ず DB へのアクセスが発生します。
- 無効化を伴う永続キャッシュなら、不要な DB アクセスも古いデータの読み取りもゼロにできます。
- TTL が適切なのは、データの変更時期を把握できない場合(サードパーティ API など)に限られます。
ステップ 1:キャッシュ基盤を確認する
# Check for Redis
grep -rn "redis\|Redis\|REDIS_URL" --include="*.json" --include="*.env*" --include="*.yml" --include="*.yaml" --include="*.rb" --include="*.py" --include="*.js" --include="*.ts" . 2>/dev/null | grep -v node_modules | head -10
# Check for Memcached
grep -rn "memcached\|Memcached\|MEMCACHE" --include="*.json" --include="*.env*" --include="*.yml" . 2>/dev/null | grep -v node_modules | head -5
# Check for in-process cache (node-cache, lru-cache, etc.)
grep -rn "lru-cache\|node-cache\|memory-cache\|caffeine\|guava.*cache" --include="*.json" --include="*.js" --include="*.ts" --include="*.java" . 2>/dev/null | grep -v node_modules | head -5
利用可能なものを報告します。キャッシュ層がない場合は、デフォルトとして Redis を推奨し、クライアントライブラリの追加を提案します。
ステップ 2:キャッシュ可能な DB 読み取りを特定する
DB クエリのパターンを調べ、キャッシュ可能性に基づいて分類します。
有力なキャッシュ候補(頻繁に読み取られ、変更頻度が低いデータ):
- ユーザープロフィール、設定、環境設定
- 商品やアイテムの詳細
- 設定テーブル
- 参照データ(DB に保存されたカテゴリ、タグ、列挙値)
- 計算コストの高い集計値(件数、合計など)
キャッシュに不向きな候補(対象外にするもの):
- リアルタイムデータ(ライブ在庫数、チャットメッセージ)
- セッション単位のデータ
- 書き込みのたびに変更されるデータ(リクエストごとに増加するカウンターなど)
# Find the most-called query patterns
grep -rn "\.find\b\|\.findById\|\.findOne\|\.where\|SELECT" \
--include="*.rb" --include="*.py" --include="*.js" --include="*.ts" \
. 2>/dev/null | grep -v node_modules | grep -v test | grep -v spec | head -30
候補ごとに、以下を記録します。
- キャッシュキーのパターン(例:
user:{id}、product:{slug}) - 書き込みや更新を行う箇所(無効化を追加する場所を特定するため)
ステップ 3:キャッシュファーストの読み取りを実装する
キャッシュ可能な読み取りごとに、以下のパターンを適用します。
Redis(Node.js / TypeScript)
async function getUser(id: string) {
const cacheKey = `user:${id}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
const user = await db.users.findById(id);
await redis.set(cacheKey, JSON.stringify(user)); // no TTL — permanent until invalidated
return user;
}
Redis(Ruby / Rails)
def find_user(id)
Rails.cache.fetch("user:#{id}") do
User.find(id) # only called on cache miss
end
end
Redis(Python)
def get_user(user_id: int):
cache_key = f"user:{user_id}"
cached = redis_client.get(cache_key)
if cached:
return json.loads(cached)
user = db.session.get(User, user_id)
redis_client.set(cache_key, json.dumps(user.to_dict())) # no expiry
return user
Play Framework(Scala)
def getUser(id: Long): Future[User] = {
cache.getOrElseUpdate(s"user:$id") {
userRepository.findById(id)
}
}
ステップ 4:書き込み時のキャッシュ無効化を実装する
キャッシュされたデータを変更する箇所すべてで、DB 書き込みの成功直後に無効化を追加します。
Node.js / TypeScript
async function updateUser(id: string, data: Partial<User>) {
const user = await db.users.update(id, data);
await redis.del(`user:${id}`); // invalidate on change
return user;
}
Ruby / Rails
def update_user(id, attrs)
user = User.find(id).tap { |u| u.update!(attrs) }
Rails.cache.delete("user:#{id}")
user
end
重要: 削除時も無効化してください。
async function deleteUser(id: string) {
await db.users.delete(id);
await redis.del(`user:${id}`);
}
リストやコレクションの場合は、項目が変更されるたびにコレクションキーを無効化します。
await redis.del(`user:${id}`); // the specific item
await redis.del(`users:list`); // any cached list that included this item
ステップ 5:キャッシュキーの規約
コードベース全体で一貫したキーの命名規則を使用します。
<entity>:<identifier> → user:123, product:slug-name
<entity>:list:<filter> → products:list:category-5
<entity>:<id>:<relation> → user:123:orders
aggregate:<entity>:<filter> → aggregate:orders:user-123-total
キャッシュモジュールの先頭にコメントを追加し、規約を記載します。
// Cache key conventions:
// user:{id} — single user record
// users:list — paginated user list (invalidate on any user change)
// user:{id}:orders — orders belonging to a user
ステップ 6:キャッシュのウォームアップ(任意)
コールドスタート時のキャッシュミスを絶対に避ける必要がある重要なデータについては、アプリケーションの起動時にウォームアップ処理を追加します。
async function warmCache() {
const activeUsers = await db.users.findActiveUsers();
await Promise.all(
activeUsers.map(u => redis.set(`user:${u.id}`, JSON.stringify(u)))
);
}
ステップ 7:概要
実装した内容を報告します。
## Cache Strategy Applied
### Cache layer: Redis (permanent, no TTL)
### Reads cached (X total)
- user:{id} — UserService.getUser()
- product:{slug} — ProductService.findBySlug()
- ...
### Invalidation points added (Y total)
- user:{id} — UserService.updateUser(), UserService.deleteUser()
- product:{slug} — ProductService.updateProduct()
- ...
### Estimated DB load reduction
Before: ~X DB queries/min
After: ~Y DB queries/min (first-read only, then cache)
注:サードパーティ API や、書き込みを捕捉できないソースから取得するデータには、代替策として長い TTL(分単位ではなく時間単位)を使用します。ただし、無効化 Webhook が利用可能になったときに再検討できるよう、これらは別途明記してください。