Redisの活用事例:キャッシュ、キュー、レート制限
数人のユーザーなら問題なく動作するWebアプリを構築したものの、トラフィックが増えるにつれて、ページの読み込みが遅くなったり、バックグラウンドジョブが重複したり、APIが悪意のあるクライアントから攻撃されたりすることに気づくでしょう。これらはRedisがエレガントに解決できる典型的なスケーリングの問題です。Redisはインメモリデータストアであり、キャッシュ、キュー、レート制限という3つの中核タスクに優れています。この記事では、各ユースケースを実践的な例とベストプラクティスとともに探っていきます。
なぜこれらのユースケースにRedisなのか?
Redisはデータをメモリに保存するため、信じられないほど高速で、多くの場合サブミリ秒の応答時間を実現します。文字列、リスト、セット、ソート済みセット、ハッシュなどの豊富なデータ構造をサポートしており、これらはキャッシュ、キュー、レート制限に自然にマッピングされます。また、アトミック操作、Pub/Sub、永続化オプションも提供しており、バックエンド開発者にとって多用途なツールとなっています。
1. Redisによるキャッシュ
キャッシュは最も一般的なRedisのユースケースです。頻繁にアクセスされるデータをRedisに保存することで、データベースの負荷を減らし、応答を高速化できます。
キャッシュの仕組み
リクエストが来たら、データがRedisに存在するか確認します。存在すれば(キャッシュヒット)、それを直接返します。存在しなければ(キャッシュミス)、データベースから取得し、有効期限付きでRedisに保存してから返します。このパターンはキャッシュアサイドと呼ばれます。
// Node.js example using ioredis
const Redis = require('ioredis');
const redis = new Redis();
async function getUser(userId) {
const cacheKey = `user:${userId}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]);
await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600); // 1 hour TTL
return user;
}
キャッシュのベストプラクティス
- TTLを設定する: 古いデータやメモリの肥大化を避けるために、常に有効期限を設定しましょう。
- 適切なデータ構造を使用する: 単純なキーと値には文字列を使用します。オブジェクトにはハッシュを使用して、フィールドを個別に更新できるようにします。
- キャッシュの無効化を処理する: 基になるデータが変更されたら、キャッシュエントリを更新または削除します。
- ヒット率を監視する: ヒット率が低い場合、キャッシュが効果的でないことを意味します。キーやTTLを調整しましょう。
キャッシュ無効化戦略
無効化は難しい問題です。一般的な戦略には次のものがあります:
- 時間ベース: TTLに依存します。シンプルですが、古いデータを提供する可能性があります。
- ライトスルー: データベースを更新するたびにキャッシュも更新します。
- ライトビハインド: まずキャッシュを更新し、その後非同期でデータベースを更新します(データ損失のリスクがあります)。
2. Redisによるキュー
キューは、時間のかかるタスクをリクエスト・レスポンスサイクルから分離します。たとえば、メールの送信、画像の処理、レポートの生成などを非同期で行うことができます。
シンプルなキューの実装
Redisのリストはキューに最適です。LPUSHで左側にジョブを追加し、BRPOPで右側からブロックしてポップします(FIFO)。
// Producer: add job
await redis.lpush('email_queue', JSON.stringify({ to: 'user@example.com', subject: 'Welcome' }));
// Consumer: process job (in a worker)
while (true) {
const job = await redis.brpop('email_queue', 0); // 0 = block indefinitely
const { to, subject } = JSON.parse(job[1]);
await sendEmail(to, subject);
}
BRPOPLPUSHによる信頼性の高いキュー
ワーカーがクラッシュした場合にジョブを失わないように、BRPOPLPUSHを使用してジョブをアトミックに処理リストに移動します。処理が成功したら、処理リストから削除します。ワーカーが死んだ場合、別のワーカーが処理リストからジョブを回復できます。
const job = await redis.brpoplpush('email_queue', 'processing_queue', 0);
try {
// process job
await redis.lrem('processing_queue', 1, job);
} catch (err) {
// handle error, maybe requeue
}
専用のキューライブラリ
本番環境では、Bull(Node.js)、RQ(Python)、Sidekiq(Ruby)などのライブラリを検討してください。これらはリトライ、スケジューリング、モニタリングをすぐに利用できる形で提供します。
3. Redisによるレート制限
レート制限はAPIを悪用から保護し、公平な使用を保証します。Redisのアトミック操作はリクエストのカウントに理想的です。
固定ウィンドウ方式のレート制限
時間ウィンドウごとにユーザーごとのカウンタをインクリメントします。カウントが制限を超えたら、リクエストを拒否します。
async function isAllowed(userId, limit = 100, windowSec = 60) {
const key = `rate:${userId}:${Math.floor(Date.now() / 1000 / windowSec)}`;
const count = await redis.incr(key);
if (count === 1) await redis.expire(key, windowSec);
return count