実践Redisキャッシング:DBダウンの課題からミリオンリクエストを支えるシステムへ

Development tutorial - IT technology blog
Development tutorial - IT technology blog

実録:データベースが「息切れ」したときの話

半年前、私が管理していたシステムはマーケティング部門がキャンペーンを実施するたびにハングアップを繰り返していました。AWSのダッシュボードでは、RDS PostgreSQLのCPU使用率が常に95%に達していました。当時、垂直スケーリング(Vertical Scaling)によるスペックアップを検討しましたが、月額400ドル近くコストが増える割に根本的な解決にはなりませんでした。分析の結果、クエリの80%が繰り返されるSELECT文であることが判明。そこで、データベースの「盾」としてRedisを導入することにしました

Redisを単なる一時的なストレージと考える人も多いですが、明確なキャッシング戦略がないと、古いデータ(Stale Data)の問題や、ピーク時にシステムをダウンさせるキャッシュスタンピード(Cache Stampede)にすぐ直面することになります。

Dockerで素早くデプロイ:リソース制限を忘れずに

私はローカル環境からサーバーまで環境を統一するために、常にDockerを優先して使用しています。Alpine版を使用することで、イメージが軽量になり、起動も速くなります。

# サーバーを保護するためにRAMを512MBに制限してRedisを実行
docker run --name redis-itfromzero \
  -d -p 6379:6379 \
  redis:alpine \
  redis-server --maxmemory 512mb --maxmemory-policy allkeys-lru

重要な注意点: allkeys-lru パラメータは、メモリがいっぱいになった際の救世主です。これにより、Redisは OOM (Out Of Memory) エラーを返してアプリケーションを中断させる代わりに、使用頻度の低いキーを自動的に削除します。

Cache-Aside戦略:シンプルだが細部へのこだわりが必要

これは最も一般的なモデルです。処理フロー:アプリがRedisを確認し、データがあれば(Cache Hit)すぐに返します(通常2〜5ms)。なければ(Cache Miss)、アプリはDBに問い合わせを行い、次回のアクセスのためにRedisに保存します。

Pythonによる実装例

import redis
import json

r = redis.Redis(host='localhost', port=6379, db=0)

def get_product_detail(product_id):
    cache_key = f"product:{product_id}"
    # 1. キャッシュを確認
    cached_data = r.get(cache_key)
    if cached_data:
        return json.loads(cached_data)
    
    # 2. DBに問い合わせ(200msの遅延をシミュレート)
    product_from_db = db.query(f"SELECT * FROM products WHERE id={product_id}")
    
    # 3. TTLを1時間に設定してキャッシュを保存
    r.setex(cache_key, 3600, json.dumps(product_from_db))
    return product_from_db

私の苦い経験:DBのデータが変更されたときにキャッシュを削除するのを決して忘れないでください。以前、価格を更新した際に r.delete(cache_key) を忘れたことがあり、その結果、お客様にはキャンペーン価格が表示されているのに、決済時には古い価格が適用されてしまうという事態が発生しました。

Write-Through:データの絶対的な整合性が必要なとき

Cache-Asideとは対照的に、Write-ThroughはDBと同時にRedisにもデータを書き込みます。この方法により、キャッシュは常に最新の状態に保たれ、古いデータが残るリスクが完全になくなります。

私は通常、ウォレットの残高やユーザーセッションなどの機密性の高い情報にこれを適用します。両方の書き込み完了を待つ必要があるため、書き込みのレイテンシはわずかに増加しますが、引き換えにデータの正確性という安心感を得られます。

TTL管理の技術とJitterテクニック

TTL(Time To Live)の設定は非常に悩ましい問題です。本番運用を経て、私は主に以下の3つのグループに分けています:

  • 変動の少ないデータ(カテゴリ、設定など): TTL 24時間〜48時間。
  • 頻繁に更新されるコンテンツ(商品、ニュースなど): TTL 1時間〜6時間。
  • ホットデータ(在庫数など): 極めて短いTTL(30秒〜2分)またはキャッシュしない。

キャッシュアバランシェ(Cache Avalanche:大量のキーが同時に期限切れになりDBがダウンする現象)を避けるために、Jitterテクニックを使用しましょう。TTLを3600秒に固定するのではなく、3600 + random(0, 300) のようにして期限切れのタイミングを分散させます。

効果的な監視:キャッシュをブラックボックスにしない

戦略が正しいかどうかをどうやって判断しますか? redis-cli info stats コマンドを使用して、 keyspace_hitskeyspace_misses の指標を確認しましょう。

Cache Hit Ratio は80%以上を目指すべきです。この数値が低すぎる場合は、あまりアクセスされないデータのためにRedisのリソースを浪費している可能性があります。また、本番環境での monitor コマンドの使用には注意が必要です。ログ出力のためにRedisサーバーのパフォーマンスが30〜50%低下することがあります。

最適化の結果は? RDSのCPU使用率は95%から15〜20%に安定しました。さらに重要なのは、重要なページのロード時間が100ms未満になり、ユーザーエクスペリエンスが劇的に向上したことです。

Share: