Redis とは?
Redis(Remote Dictionary Server)は、インメモリのキーバリューデータストアです。データをメモリ上に保持するため、ディスクベースのDBと比べて桁違いに高速です。
Redis の特徴
| 特徴 | 説明 |
|---|---|
| 超高速 | メモリ上で動作、1秒に10万回以上の操作が可能 |
| 豊富なデータ型 | String, Hash, List, Set, Sorted Set, Stream等 |
| 永続化 | RDB/AOFでディスクに保存可能 |
| Pub/Sub | メッセージング機能 |
| TTL | キーごとに有効期限を設定 |
| Atomic操作 | 単一コマンドはアトミックに実行 |
インストールと起動
# macOS
brew install redis
redis-server
# Docker
docker run -d --name redis -p 6379:6379 redis:7-alpine
# 接続確認
redis-cli ping
# PONG
基本的なデータ型とコマンド
String(文字列)
最も基本的なデータ型です。
# 設定と取得
SET user:1:name "田中太郎"
GET user:1:name
# "田中太郎"
# 有効期限付き(60秒)
SET session:abc123 "user_data" EX 60
# 存在しない場合だけ設定(ロックに使える)
SET lock:resource1 "owner1" NX EX 30
# カウンター
SET page:views 0
INCR page:views # 1
INCR page:views # 2
INCRBY page:views 10 # 12
Hash(ハッシュ)
オブジェクトの保存に最適です。
# ハッシュに複数フィールドを設定
HSET user:1 name "田中太郎" email "[email protected]" age 30
# 1つのフィールドを取得
HGET user:1 name
# "田中太郎"
# 全フィールドを取得
HGETALL user:1
# 1) "name"
# 2) "田中太郎"
# 3) "email"
# 4) "[email protected]"
# 5) "age"
# 6) "30"
# フィールドの存在チェック
HEXISTS user:1 email
# (integer) 1
List(リスト)
順序付きの要素リストです。キューやスタックとして使えます。
# 左から追加(キューの入口)
LPUSH queue:tasks "task1" "task2" "task3"
# 右から取得(キューの出口)
RPOP queue:tasks
# "task1"
# 範囲取得
LRANGE queue:tasks 0 -1
# 1) "task3"
# 2) "task2"
# リストの長さ
LLEN queue:tasks
# (integer) 2
Set(セット)
重複のないユニークな要素の集合です。
# 追加
SADD tags:article1 "JavaScript" "TypeScript" "React"
SADD tags:article2 "TypeScript" "Vue" "React"
# メンバー一覧
SMEMBERS tags:article1
# 共通のタグ(積集合)
SINTER tags:article1 tags:article2
# 1) "TypeScript"
# 2) "React"
# メンバー数
SCARD tags:article1
# (integer) 3
Sorted Set(ソート済みセット)
スコア付きの順序付きセットです。ランキングに最適です。
# スコア付きで追加
ZADD ranking 100 "user:1"
ZADD ranking 250 "user:2"
ZADD ranking 180 "user:3"
# スコアの高い順に取得
ZREVRANGE ranking 0 2 WITHSCORES
# 1) "user:2"
# 2) "250"
# 3) "user:3"
# 4) "180"
# 5) "user:1"
# 6) "100"
# スコアを増加
ZINCRBY ranking 50 "user:1"
# "150"
# ランク取得(0始まり、高い順)
ZREVRANK ranking "user:2"
# (integer) 0
キャッシュ戦略
Cache-Aside(Lazy Loading)
最も一般的なパターンです。
1. キャッシュを確認
2. ヒット → キャッシュから返す
3. ミス → DBから取得 → キャッシュに保存 → 返す
async function getUser(userId) {
const cacheKey = `user:${userId}`;
// 1. キャッシュを確認
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached); // キャッシュヒット
}
// 2. DBから取得
const user = await db.users.findById(userId);
// 3. キャッシュに保存(TTL: 1時間)
await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600);
return user;
}
メリット: 読み取り時のみキャッシュを作成するのでシンプル
デメリット: 初回アクセスは必ずキャッシュミスが発生
Write-Through
書き込み時にキャッシュも同時に更新します。
async function updateUser(userId, data) {
const cacheKey = `user:${userId}`;
// 1. DBを更新
const user = await db.users.update(userId, data);
// 2. キャッシュも更新
await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600);
return user;
}
メリット: キャッシュとDBの一貫性が高い
デメリット: 書き込みが2倍のコスト
Write-Behind(Write-Back)
キャッシュに書き込み、非同期でDBに反映します。
async function updateUser(userId, data) {
const cacheKey = `user:${userId}`;
// 1. キャッシュを更新(即座に返す)
await redis.set(cacheKey, JSON.stringify(data), 'EX', 3600);
// 2. 非同期でDBに書き込み(キューに入れる)
await redis.lpush('db:write-queue', JSON.stringify({
table: 'users',
id: userId,
data,
}));
return data;
}
メリット: 書き込みが非常に高速
デメリット: データ損失のリスクがある
TTL(有効期限)の設定
TTL の基本
# 設定時にTTLを指定(秒)
SET key "value" EX 3600
# 設定時にTTLを指定(ミリ秒)
SET key "value" PX 60000
# 既存のキーにTTLを設定
EXPIRE key 3600
# TTLを確認
TTL key
# (integer) 3598
# TTLを削除(永続化)
PERSIST key
TTL の設計指針
| データの種類 | 推奨TTL | 理由 |
|---|---|---|
| セッション | 30分〜24時間 | セキュリティとUXのバランス |
| ユーザープロフィール | 1〜24時間 | 頻繁に変わらない |
| API レスポンス | 5〜60分 | 鮮度とパフォーマンスのバランス |
| ランキング | 1〜5分 | リアルタイム性が重要 |
| 設定値 | 1〜7日 | ほぼ変わらない |
Thundering Herd 問題の対策
人気のキーが同時に期限切れになると、大量のDBクエリが同時に発生します。
// TTLにランダムなジッター(ゆらぎ)を追加
function getTTLWithJitter(baseTTL) {
const jitter = Math.floor(Math.random() * baseTTL * 0.1);
return baseTTL + jitter;
}
await redis.set(key, value, 'EX', getTTLWithJitter(3600));
// 3600〜3960秒のランダムなTTL
実装例(Node.js)
接続
import { createClient } from 'redis';
const redis = createClient({
url: process.env.REDIS_URL || 'redis://localhost:6379',
});
redis.on('error', (err) => console.error('Redis Error:', err));
await redis.connect();
キャッシュミドルウェア(Express)
function cacheMiddleware(ttl = 300) {
return async (req, res, next) => {
const key = `cache:${req.originalUrl}`;
try {
const cached = await redis.get(key);
if (cached) {
return res.json(JSON.parse(cached));
}
} catch (err) {
console.error('Cache read error:', err);
}
// レスポンスをインターセプト
const originalJson = res.json.bind(res);
res.json = async (data) => {
try {
await redis.set(key, JSON.stringify(data), { EX: ttl });
} catch (err) {
console.error('Cache write error:', err);
}
return originalJson(data);
};
next();
};
}
// 使い方
app.get('/api/users', cacheMiddleware(600), async (req, res) => {
const users = await db.users.findAll();
res.json(users);
});
セッション管理
// セッションの保存
async function createSession(userId) {
const sessionId = crypto.randomUUID();
const sessionData = { userId, createdAt: Date.now() };
await redis.set(
`session:${sessionId}`,
JSON.stringify(sessionData),
{ EX: 86400 } // 24時間
);
return sessionId;
}
// セッションの取得
async function getSession(sessionId) {
const data = await redis.get(`session:${sessionId}`);
return data ? JSON.parse(data) : null;
}
// セッションの延長
async function refreshSession(sessionId) {
await redis.expire(`session:${sessionId}`, 86400);
}
レートリミット
async function rateLimit(identifier, limit = 100, windowSec = 60) {
const key = `rate:${identifier}`;
const current = await redis.incr(key);
if (current === 1) {
await redis.expire(key, windowSec);
}
if (current > limit) {
const ttl = await redis.ttl(key);
return { allowed: false, retryAfter: ttl };
}
return { allowed: true, remaining: limit - current };
}
運用のベストプラクティス
メモリ管理
# メモリ使用量を確認
redis-cli INFO memory
# 最大メモリを設定
CONFIG SET maxmemory 256mb
# 退避ポリシーを設定
CONFIG SET maxmemory-policy allkeys-lru
| ポリシー | 説明 |
|---|---|
| noeviction | メモリ上限でエラー(デフォルト) |
| allkeys-lru | 最も長く使われていないキーを削除 |
| volatile-lru | TTL付きのキーでLRU削除 |
| allkeys-random | ランダムに削除 |
| volatile-ttl | TTLが短いキーから削除 |
キー命名規則
# 推奨: コロン区切り
user:123:profile
session:abc123
cache:api:/users
rate:ip:192.168.1.1
# 非推奨
user_123_profile
UserProfile_123
まとめ
| パターン | 用途 | 推奨キャッシュ戦略 |
|---|---|---|
| 読み取り多い | ユーザー情報、商品データ | Cache-Aside |
| 読み書き均等 | 更新頻度の高いデータ | Write-Through |
| 書き込み多い | ログ、イベントデータ | Write-Behind |
| リアルタイム | ランキング、カウンター | 直接Redis |
Redisは適切に使えばアプリケーションのパフォーマンスを劇的に向上させます。まずはCache-Asideパターンから始め、必要に応じて他のパターンを検討しましょう。
Redis に格納するJSON データの構造確認には、AssistyのJSONフォーマッターが便利です。