深夜2時、Djangoサーバーが亀のように遅くなった — 足りなかったのはRedisだった
当時、モバイルアプリ向けに商品一覧を返すDjango APIを動かしていた。トラフィックが少ないときは問題なかった。でもセール期間になると、サーバーがタイムアウトし始めた。確認してみると、MySQLが約400クエリ/秒を処理していて、その大半はフィルターパラメーターが少し違うだけの全く同じSELECT文だった。
原因はすぐわかった:結果が5分間変わらないのに、毎回リクエストがデータベースに直接アクセスしていた。これがキャッシュレイヤーの欠如というやつだ。
シンプルなクエリなのになぜデータベースが過負荷になるのか?
クライアントが/products?category=shoesというAPIを呼ぶたびに、DjangoはフルSQLクエリを実行し、MySQLはパース → インデックス検索 → ディスク読み取り(またはバッファープール) → レスポンスという処理を行う。400クエリ/秒でその80%が重複している場合、MySQLのCPUはほぼ同じ処理の繰り返しに費やされてしまう。
最もシンプルな解決策:中間にキャッシュレイヤーを追加することだ。初回は実際にクエリを実行して結果をキャッシュに保存する。次回以降はキャッシュから返すため、データベースには一切アクセスしない。これがまさにRedisが最も得意とすることだ。
LinuxへのRedisインストール — 手早くきれいに
Ubuntu / Debian
# Redisの公式リポジトリからインストール(最新版、Ubuntuの古いパッケージではない)
curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list
sudo apt update
sudo apt install redis -y
# 状態を確認
sudo systemctl status redis
sudo systemctl enable redis
CentOS / RHEL / Rocky Linux
sudo dnf install epel-release -y
sudo dnf install redis -y
sudo systemctl start redis
sudo systemctl enable redis
Redisの動作確認
redis-cli ping
# 結果:PONG
PONGが表示されれば完了だ。表示されない場合はsystemctl status redisを確認しよう — 99%の確率でサービスが起動していないか、ポート6379がファイアウォールでブロックされている。
実務でよく使うRedisコマンド
SETとGET — 最もシンプル
redis-cli
# 値を保存
SET username "hoanganh"
# 値を取得
GET username
# => "hoanganh"
# TTL付きで保存(300秒後に自動削除)
SET session_token "abc123" EX 300
# 残り秒数を確認
TTL session_token
Hash — 複数フィールドを持つオブジェクトの保存
# ユーザー情報を保存
HSET user:1001 name "田中 太郎" email "[email protected]" age 28
# 1つのフィールドを取得
HGET user:1001 name
# 全フィールドを取得
HGETALL user:1001
List — シンプルなキュー
# 先頭に追加
LPUSH job_queue "send_email"
LPUSH job_queue "resize_image"
# 末尾から取り出す(FIFO)
RPOP job_queue
# => "send_email"
キーの削除と確認
# キーが存在するか確認
EXISTS username
# => 1(存在する)または 0(存在しない)
# キーを削除
DEL username
# 全キーを表示(本番環境では使用禁止 — 危険)
KEYS *
# 本番環境ではSCANを代わりに使う
SCAN 0 MATCH user:* COUNT 100
PythonでRedisキャッシュを使う — 実践例
最初のDjango APIの問題に戻ろう。あの夜に実際に行った修正方法はこうだ:
import redis
import json
import hashlib
# Redisに接続
r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
def get_products(category: str, filters: dict) -> list:
# クエリパラメーターからキャッシュキーを生成
cache_key = f"products:{category}:{hashlib.md5(json.dumps(filters, sort_keys=True).encode()).hexdigest()}"
# キャッシュから取得を試みる
cached = r.get(cache_key)
if cached:
return json.loads(cached)
# キャッシュミス → データベースをクエリ
results = db.query("SELECT * FROM products WHERE category = %s ...", category)
# キャッシュに保存、TTL 5分
r.setex(cache_key, 300, json.dumps(results))
return results
その夜のうちにデプロイ完了。MySQLクエリは400/秒から約40/秒に減少。レスポンスタイムは800msから20msへ改善。たった30行のキャッシュコードで、サーバーのタイムアウトが解消された。
本番環境向けRedis最小設定
デフォルトの設定ファイルは/etc/redis/redis.confにある。すぐに変更すべき項目は以下だ:
# Redisが使用するRAMの上限
maxmemory 512mb
# RAMが満杯になった時:期限切れに近いキーから削除(キャッシュ用途に適切)
maxmemory-policy allkeys-lru
# localhostのみにバインド、不要なら外部に公開しない
bind 127.0.0.1
# パスワードを設定(外部ポートが開いている場合は必須)
requirepass your_strong_password_here
# ディスクへのスナップショット保存(RDBパーシステンス)
save 900 1
save 300 10
save 60 10000
設定変更後:
sudo systemctl restart redis
# 認証テスト
redis-cli -a your_strong_password_here ping
Redisを適切な用途に使う — 向いていない場所に無理やり使わない
Redisは強力だが、すべての問題を解決できるわけではない。あるチームがRedisを注文のプライマリデータベースとして使っているのを見たことがある。パーシステンスを有効にしていなかったため、サーバーを再起動したら3日分のトランザクションデータが全部消えた。高い授業料だった:本番データに触れる前に、各ツールの役割を正しく理解しておくべきだ。
Redisが最も適しているのは:
- APIレスポンスのキャッシュ — データベースの負荷を軽減し、読み取り速度を向上
- セッションストレージ — ユーザーセッション、認証トークンの保存
- レートリミット — IPごとのリクエスト数カウント、スパム対策
- ジョブキュー — CeleryやRQとの組み合わせ
- Pub/Subメッセージング — シンプルなリアルタイム通知
Redisに向いていないのは:
- 絶対的な永続性が必要なデータ(注文、金融取引)
- 複雑なリレーショナルデータ(複数テーブルのjoin)
- 大きなファイルやバイナリblob
Redisの簡易モニタリング
# リアルタイム統計を確認
redis-cli info stats
# キャッシュのヒット率
redis-cli info stats | grep -E 'keyspace_hits|keyspace_misses'
# リアルタイムで実行中のコマンドを確認(topコマンドのように、ただしRedis用)
redis-cli monitor
keyspace_hitsがkeyspace_missesを大幅に上回っていれば、キャッシュは正常に機能している。逆の場合はTTLとキャッシュキーのロジックを見直そう。
Redisは難しくない。初めてインストールしてもすぐに使い始められた。問題はインストールではなく、いつ使うべきかを判断し、予期せぬ障害が起きないよう正しく設定することだ。次回、深夜にサーバーがもたつき始めたら、まずキャッシュレイヤーを確認してみよう — 高確率でそこが原因のはずだ。

