ストーリー
「APIのレスポンスが遅い。毎回同じデータをDBから取得してる」
高橋アーキテクトがAPMのグラフを見せる。同じクエリが秒間1000回実行されている。
「Redisを使おう。インメモリだから桁違いに速い。しかもRedisは単なるキャッシュじゃない。データ構造が豊富で、キュー、ランキング、Pub/Subまでカバーできる万能ツールだ」
Redisの基本
| 特徴 | 説明 |
|---|---|
| インメモリ | データをメモリに格納(超高速) |
| データ構造 | String, Hash, List, Set, Sorted Set, Stream |
| TTL | キーごとに有効期限を設定 |
| 永続化 | RDB/AOFでディスクにも保存可能 |
| シングルスレッド | コマンドはアトミックに実行 |
パターン1: キャッシュ
最も基本的な用途。DBクエリ結果をキャッシュしてレスポンスを高速化。
import Redis from 'ioredis';
const redis = new Redis();
// Cache-Aside パターン
async function getUserById(userId: number): Promise<User | null> {
const cacheKey = `user:${userId}`;
// 1. キャッシュを確認
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// 2. キャッシュミス → DBから取得
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
if (!user) return null;
// 3. キャッシュに格納(TTL: 5分)
await redis.set(cacheKey, JSON.stringify(user), 'EX', 300);
return user;
}
// キャッシュ無効化
async function updateUser(userId: number, data: Partial<User>): Promise<void> {
await db.query('UPDATE users SET ... WHERE id = $1', [userId]);
await redis.del(`user:${userId}`); // キャッシュを削除
}
パターン2: セッション管理
// セッション格納(Hash型)
async function createSession(sessionId: string, userId: number): Promise<void> {
const key = `session:${sessionId}`;
await redis.hset(key, {
userId: userId.toString(),
loginAt: new Date().toISOString(),
lastAccess: new Date().toISOString(),
});
await redis.expire(key, 3600); // 1時間で期限切れ
}
// セッション取得
async function getSession(sessionId: string): Promise<SessionData | null> {
const key = `session:${sessionId}`;
const data = await redis.hgetall(key);
if (!data.userId) return null;
// 最終アクセス時刻を更新(スライディングウィンドウ)
await redis.hset(key, 'lastAccess', new Date().toISOString());
await redis.expire(key, 3600); // TTLをリセット
return {
userId: parseInt(data.userId),
loginAt: new Date(data.loginAt),
lastAccess: new Date(data.lastAccess),
};
}
パターン3: ランキング(Sorted Set)
// ランキング(Sorted Set)
async function addScore(userId: string, score: number): Promise<void> {
await redis.zadd('leaderboard', score, userId);
}
// トップ10を取得(スコア降順)
async function getTopRankers(limit: number = 10): Promise<RankEntry[]> {
const results = await redis.zrevrange('leaderboard', 0, limit - 1, 'WITHSCORES');
const entries: RankEntry[] = [];
for (let i = 0; i < results.length; i += 2) {
entries.push({
userId: results[i],
score: parseFloat(results[i + 1]),
rank: i / 2 + 1,
});
}
return entries;
}
// 特定ユーザーの順位
async function getUserRank(userId: string): Promise<number | null> {
const rank = await redis.zrevrank('leaderboard', userId);
return rank !== null ? rank + 1 : null;
}
パターン4: レートリミット
// スライディングウィンドウ方式のレートリミット
async function checkRateLimit(
userId: string,
maxRequests: number = 100,
windowSeconds: number = 60
): Promise<{ allowed: boolean; remaining: number }> {
const key = `ratelimit:${userId}`;
const now = Date.now();
const windowStart = now - windowSeconds * 1000;
// トランザクションで原子的に実行
const pipeline = redis.pipeline();
pipeline.zremrangebyscore(key, 0, windowStart); // 古いエントリを削除
pipeline.zadd(key, now, `${now}`); // 新しいリクエストを追加
pipeline.zcard(key); // カウント取得
pipeline.expire(key, windowSeconds); // TTL設定
const results = await pipeline.exec();
const currentCount = results?.[2]?.[1] as number ?? 0;
return {
allowed: currentCount <= maxRequests,
remaining: Math.max(0, maxRequests - currentCount),
};
}
パターン5: Pub/Sub(リアルタイム通知)
// Publisher
async function publishNotification(channel: string, message: object): Promise<void> {
await redis.publish(channel, JSON.stringify(message));
}
// Subscriber
const subscriber = new Redis();
subscriber.subscribe('notifications:user:1');
subscriber.on('message', (channel, message) => {
const data = JSON.parse(message);
console.log(`Channel: ${channel}, Message:`, data);
});
// 使用例: 新しいメッセージ通知
await publishNotification('notifications:user:1', {
type: 'new_message',
from: 'user:2',
preview: 'こんにちは!',
});
パターン6: 分散ロック
// 分散ロック(Redlock パターンの簡易版)
async function acquireLock(
lockKey: string,
ttlMs: number = 5000
): Promise<string | null> {
const lockValue = crypto.randomUUID();
const result = await redis.set(
`lock:${lockKey}`,
lockValue,
'PX', ttlMs,
'NX' // キーが存在しない場合のみ設定
);
return result === 'OK' ? lockValue : null;
}
async function releaseLock(lockKey: string, lockValue: string): Promise<boolean> {
// Luaスクリプトで原子的にチェック&削除
const script = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`;
const result = await redis.eval(script, 1, `lock:${lockKey}`, lockValue);
return result === 1;
}
// 使用例
const lockValue = await acquireLock('inventory:product:123');
if (lockValue) {
try {
// クリティカルセクション: 在庫更新
await updateInventory(123, -1);
} finally {
await releaseLock('inventory:product:123', lockValue);
}
}
Redisのデータ構造早見表
| データ構造 | 用途 | 主要コマンド |
|---|---|---|
| String | キャッシュ、カウンター | GET, SET, INCR |
| Hash | オブジェクト格納 | HGET, HSET, HGETALL |
| List | キュー、タイムライン | LPUSH, RPOP, LRANGE |
| Set | ユニーク集合、タグ | SADD, SMEMBERS, SINTER |
| Sorted Set | ランキング、スコア | ZADD, ZRANGE, ZRANK |
| Stream | イベントログ | XADD, XREAD, XRANGE |
まとめ
| パターン | データ構造 | 用途 |
|---|---|---|
| キャッシュ | String | DB負荷軽減、高速化 |
| セッション | Hash | ユーザー状態管理 |
| ランキング | Sorted Set | スコアベースの順位付け |
| レートリミット | Sorted Set | API呼び出し制限 |
| Pub/Sub | - | リアルタイム通知 |
| 分散ロック | String (NX) | 排他制御 |
理解度チェックリスト
- Cache-Asideパターンを実装できる
- Redisの6つのデータ構造の用途を説明できる
- Sorted Setを使ったランキング機能を実装できる
- 分散ロックの仕組みを理解している
次のステップ
次のレッスンではRDB vs NoSQLの選択の判断基準を学ぶ。実際のプロジェクトでどちらを選ぶべきか、体系的な判断フレームワークを身につけよう。
推定読了時間: 30分