Redis完全ガイド2026 — キャッシュからメッセージキューまで
Redisは世界で最も人気のあるインメモリデータストアの1つです。キャッシュ、セッション管理、リアルタイムアプリケーション、メッセージキューなど、様々な用途で活用されています。本記事では、2026年版のRedis完全ガイドとして、基本から実践的な活用法まで解説します。
Redisとは
Redisは「Remote Dictionary Server」の略で、オープンソースのインメモリデータ構造ストアです。主な特徴は以下の通りです。
- 高速: すべてのデータをメモリに保持するため、読み書きが極めて高速
- 多様なデータ構造: 文字列、リスト、セット、ハッシュなど豊富なデータ型
- 永続化: メモリベースでありながら、データの永続化も可能
- レプリケーション: マスター/スレーブ構成でデータの冗長化
- クラスタリング: 水平スケーリングに対応
なぜRedisを使うのか
- パフォーマンス向上: データベースへの負荷を軽減し、応答速度を劇的に改善
- セッション管理: 分散システムでのセッション共有
- リアルタイム処理: Pub/Subやストリームを活用したイベント駆動アーキテクチャ
- ランキング機能: Sorted Setを使った効率的なランキング実装
- レート制限: API呼び出し回数の制限など
Redisの基本データ構造
Redisは単純なKey-Valueストアではなく、複数のデータ構造をサポートしています。
1. String(文字列)
最もシンプルなデータ型。テキストだけでなく、バイナリデータも格納可能です。
# 基本的な操作
SET user:1000:name "Alice"
GET user:1000:name
# => "Alice"
# 有効期限付き設定(60秒後に自動削除)
SETEX session:abc123 60 "user_data"
# 数値の増減
SET page:views 0
INCR page:views
# => 1
INCRBY page:views 10
# => 11
# 複数のキーを一度に操作
MSET key1 "value1" key2 "value2"
MGET key1 key2
実用例: カウンター
// Node.jsでのページビューカウンター
const redis = require('redis');
const client = redis.createClient();
async function incrementPageView(pageId) {
await client.incr(`page:${pageId}:views`);
const views = await client.get(`page:${pageId}:views`);
return parseInt(views);
}
2. List(リスト)
順序付きの文字列コレクション。キューやスタックとして利用できます。
# 左側(先頭)に追加
LPUSH tasks "task1"
LPUSH tasks "task2"
# 右側(末尾)に追加
RPUSH tasks "task3"
# 左側から取り出し
LPOP tasks
# => "task2"
# 範囲取得
LRANGE tasks 0 -1
# => すべての要素
# 長さ取得
LLEN tasks
実用例: タスクキュー
// タスクキューの実装
class RedisQueue {
constructor(client, queueName) {
this.client = client;
this.queueName = queueName;
}
async enqueue(task) {
await this.client.rPush(this.queueName, JSON.stringify(task));
}
async dequeue() {
const task = await this.client.lPop(this.queueName);
return task ? JSON.parse(task) : null;
}
async size() {
return await this.client.lLen(this.queueName);
}
}
3. Set(セット)
重複のない文字列の集合。メンバーシップテストや集合演算が高速です。
# 追加
SADD tags:post1 "javascript" "redis" "nodejs"
# メンバー確認
SISMEMBER tags:post1 "redis"
# => 1 (存在する)
# すべてのメンバー取得
SMEMBERS tags:post1
# 集合演算
SADD tags:post2 "python" "redis"
SINTER tags:post1 tags:post2
# => "redis" (共通要素)
SUNION tags:post1 tags:post2
# => すべてのタグ
# 削除
SREM tags:post1 "nodejs"
実用例: ユニークビジター追跡
// 日次のユニークビジター追跡
async function trackVisitor(date, userId) {
const key = `visitors:${date}`;
await client.sAdd(key, userId);
// 1日後に自動削除
await client.expire(key, 86400);
}
async function getUniqueVisitors(date) {
const key = `visitors:${date}`;
return await client.sCard(key); // セットのサイズ
}
4. Hash(ハッシュ)
フィールド-値のペアを持つオブジェクト。構造化されたデータの保存に最適です。
# フィールド設定
HSET user:1000 name "Alice" email "alice@example.com" age 30
# フィールド取得
HGET user:1000 name
# => "Alice"
# すべてのフィールド取得
HGETALL user:1000
# 複数フィールド設定
HMSET user:1001 name "Bob" email "bob@example.com"
# 数値フィールドの増減
HINCRBY user:1000 age 1
# フィールド削除
HDEL user:1000 age
実用例: ユーザープロファイル
// ユーザープロファイルの管理
class UserProfile {
constructor(client, userId) {
this.client = client;
this.key = `user:${userId}`;
}
async set(data) {
await this.client.hSet(this.key, data);
}
async get(field) {
if (field) {
return await this.client.hGet(this.key, field);
}
return await this.client.hGetAll(this.key);
}
async update(field, value) {
await this.client.hSet(this.key, field, value);
}
async incrementField(field, amount = 1) {
return await this.client.hIncrBy(this.key, field, amount);
}
}
5. Sorted Set(ソート済みセット)
スコアを持つ順序付きセット。ランキングやリーダーボードに最適です。
# スコア付きで追加
ZADD leaderboard 100 "player1"
ZADD leaderboard 200 "player2"
ZADD leaderboard 150 "player3"
# ランキング取得(降順)
ZREVRANGE leaderboard 0 2 WITHSCORES
# => "player2", 200, "player3", 150, "player1", 100
# スコア範囲で検索
ZRANGEBYSCORE leaderboard 100 150
# スコア増加
ZINCRBY leaderboard 50 "player1"
# ランク取得(0始まり)
ZREVRANK leaderboard "player2"
# => 0 (1位)
実用例: ゲームランキング
// ゲームのランキングシステム
class Leaderboard {
constructor(client, gameId) {
this.client = client;
this.key = `leaderboard:${gameId}`;
}
async addScore(playerId, score) {
await this.client.zAdd(this.key, {
score: score,
value: playerId
});
}
async incrementScore(playerId, points) {
return await this.client.zIncrBy(this.key, points, playerId);
}
async getTopPlayers(count = 10) {
const players = await this.client.zRangeWithScores(
this.key,
0,
count - 1,
{ REV: true }
);
return players.map((p, i) => ({
rank: i + 1,
playerId: p.value,
score: p.score
}));
}
async getPlayerRank(playerId) {
const rank = await this.client.zRevRank(this.key, playerId);
return rank !== null ? rank + 1 : null;
}
}
キャッシュパターン
Redisの最も一般的な用途はキャッシュです。効果的なキャッシュ戦略を理解しましょう。
1. Cache-Aside(Lazy Loading)
最も一般的なパターン。アプリケーションがキャッシュを明示的に管理します。
async function getUser(userId) {
const cacheKey = `user:${userId}`;
// 1. キャッシュを確認
let user = await redis.get(cacheKey);
if (user) {
console.log('Cache hit');
return JSON.parse(user);
}
// 2. キャッシュミスならDBから取得
console.log('Cache miss');
user = await db.users.findById(userId);
// 3. キャッシュに保存(5分間)
await redis.setEx(cacheKey, 300, JSON.stringify(user));
return user;
}
利点: シンプルで理解しやすい、必要なデータだけキャッシュ 欠点: キャッシュミス時のレイテンシ、キャッシュとDBの不整合の可能性
2. Write-Through
データを書き込む際、同時にキャッシュも更新します。
async function updateUser(userId, data) {
const cacheKey = `user:${userId}`;
// 1. DBを更新
const updatedUser = await db.users.update(userId, data);
// 2. 同時にキャッシュも更新
await redis.setEx(cacheKey, 300, JSON.stringify(updatedUser));
return updatedUser;
}
利点: キャッシュとDBの整合性が高い、読み取り時のレイテンシが低い 欠点: 書き込み時のレイテンシが増加、使われないデータもキャッシュされる可能性
3. Write-Behind(Write-Back)
キャッシュを先に更新し、非同期でDBに反映します。
async function updateUserScore(userId, score) {
const cacheKey = `user:${userId}:score`;
// 1. キャッシュを即座に更新
await redis.set(cacheKey, score);
// 2. 更新フラグを立てる
await redis.sAdd('dirty:users', userId);
// 3. バックグラウンドで定期的にDBに反映
// (別プロセスで処理)
}
// 定期実行プロセス
async function syncToDatabase() {
const dirtyUsers = await redis.sMembers('dirty:users');
for (const userId of dirtyUsers) {
const score = await redis.get(`user:${userId}:score`);
await db.users.update(userId, { score: parseInt(score) });
await redis.sRem('dirty:users', userId);
}
}
利点: 書き込みパフォーマンスが高い 欠点: 実装が複雑、障害時のデータロストリスク
キャッシュ戦略のベストプラクティス
class CacheManager {
constructor(redis, db, options = {}) {
this.redis = redis;
this.db = db;
this.ttl = options.ttl || 300; // デフォルト5分
this.prefix = options.prefix || 'cache';
}
generateKey(...parts) {
return `${this.prefix}:${parts.join(':')}`;
}
async get(key, fetchFn) {
const cacheKey = this.generateKey(key);
// キャッシュを確認
const cached = await this.redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// データを取得
const data = await fetchFn();
// キャッシュに保存
if (data !== null && data !== undefined) {
await this.redis.setEx(
cacheKey,
this.ttl,
JSON.stringify(data)
);
}
return data;
}
async invalidate(key) {
const cacheKey = this.generateKey(key);
await this.redis.del(cacheKey);
}
async invalidatePattern(pattern) {
const keys = await this.redis.keys(`${this.prefix}:${pattern}`);
if (keys.length > 0) {
await this.redis.del(keys);
}
}
}
// 使用例
const cache = new CacheManager(redis, db);
// データ取得
const user = await cache.get(`user:${userId}`, async () => {
return await db.users.findById(userId);
});
// キャッシュ無効化
await cache.invalidate(`user:${userId}`);
// パターンマッチで無効化
await cache.invalidatePattern('user:*');
セッション管理
分散システムでのセッション共有にRedisは理想的です。
Express + Redis Session
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const app = express();
const redisClient = createClient();
redisClient.connect();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 1000 * 60 * 60 * 24 // 24時間
}
}));
app.get('/login', (req, res) => {
req.session.userId = '12345';
req.session.username = 'Alice';
res.send('Logged in');
});
app.get('/profile', (req, res) => {
if (req.session.userId) {
res.json({ userId: req.session.userId, username: req.session.username });
} else {
res.status(401).send('Not authenticated');
}
});
カスタムセッションマネージャー
class SessionManager {
constructor(redis, options = {}) {
this.redis = redis;
this.ttl = options.ttl || 3600; // 1時間
this.prefix = 'session';
}
generateSessionId() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
async create(userId, data = {}) {
const sessionId = this.generateSessionId();
const key = `${this.prefix}:${sessionId}`;
const sessionData = {
userId,
createdAt: Date.now(),
...data
};
await this.redis.setEx(key, this.ttl, JSON.stringify(sessionData));
return sessionId;
}
async get(sessionId) {
const key = `${this.prefix}:${sessionId}`;
const data = await this.redis.get(key);
if (!data) return null;
// TTLを延長
await this.redis.expire(key, this.ttl);
return JSON.parse(data);
}
async update(sessionId, data) {
const key = `${this.prefix}:${sessionId}`;
const existing = await this.get(sessionId);
if (!existing) {
throw new Error('Session not found');
}
const updated = { ...existing, ...data };
await this.redis.setEx(key, this.ttl, JSON.stringify(updated));
}
async destroy(sessionId) {
const key = `${this.prefix}:${sessionId}`;
await this.redis.del(key);
}
async destroyAll(userId) {
const pattern = `${this.prefix}:*`;
const keys = await this.redis.keys(pattern);
for (const key of keys) {
const data = await this.redis.get(key);
const session = JSON.parse(data);
if (session.userId === userId) {
await this.redis.del(key);
}
}
}
}
Pub/Sub(パブリッシュ/サブスクライブ)
リアルタイムメッセージングやイベント駆動アーキテクチャに使用します。
基本的な使い方
const { createClient } = require('redis');
// パブリッシャー
const publisher = createClient();
await publisher.connect();
// サブスクライバー
const subscriber = createClient();
await subscriber.connect();
// メッセージを購読
await subscriber.subscribe('notifications', (message) => {
console.log('Received:', message);
});
// メッセージを発行
await publisher.publish('notifications', 'Hello, World!');
チャットアプリケーション
class ChatRoom {
constructor(roomId) {
this.roomId = roomId;
this.channel = `chat:${roomId}`;
this.publisher = createClient();
this.subscriber = createClient();
}
async connect() {
await this.publisher.connect();
await this.subscriber.connect();
}
async join(userId, onMessage) {
await this.subscriber.subscribe(this.channel, (message) => {
const data = JSON.parse(message);
onMessage(data);
});
await this.sendMessage('system', `${userId} joined the room`);
}
async sendMessage(userId, text) {
const message = {
userId,
text,
timestamp: Date.now()
};
await this.publisher.publish(this.channel, JSON.stringify(message));
}
async leave(userId) {
await this.sendMessage('system', `${userId} left the room`);
await this.subscriber.unsubscribe(this.channel);
}
}
// 使用例
const room = new ChatRoom('general');
await room.connect();
await room.join('Alice', (message) => {
console.log(`[${message.userId}]: ${message.text}`);
});
await room.sendMessage('Alice', 'Hello everyone!');
Redis Streams
Pub/Subより高度なメッセージング機能を提供します。
// ストリームにメッセージ追加
async function addToStream(streamKey, data) {
const id = await redis.xAdd(streamKey, '*', data);
return id;
}
// ストリームから読み取り(コンシューマーグループ)
async function consumeStream(streamKey, groupName, consumerName) {
// グループ作成(初回のみ)
try {
await redis.xGroupCreate(streamKey, groupName, '0', {
MKSTREAM: true
});
} catch (e) {
// グループが既に存在する場合は無視
}
while (true) {
// メッセージを読み取り
const messages = await redis.xReadGroup(
groupName,
consumerName,
{ key: streamKey, id: '>' },
{ COUNT: 10, BLOCK: 5000 }
);
if (messages && messages.length > 0) {
for (const [stream, msgs] of messages) {
for (const msg of msgs) {
console.log('Processing:', msg.message);
// 処理完了を確認
await redis.xAck(streamKey, groupName, msg.id);
}
}
}
}
}
// 使用例
await addToStream('events', {
type: 'user_registered',
userId: '12345',
timestamp: Date.now().toString()
});
consumeStream('events', 'processors', 'worker-1');
Redis Stack
Redis 7以降では、追加モジュールを使った拡張機能が利用可能です。
RediSearch(全文検索)
// インデックス作成
await redis.ft.create('idx:products', {
name: 'TEXT',
description: 'TEXT',
price: 'NUMERIC',
category: 'TAG'
}, {
ON: 'HASH',
PREFIX: 'product:'
});
// 商品追加
await redis.hSet('product:1', {
name: 'Redis Book',
description: 'Complete guide to Redis',
price: '2980',
category: 'books'
});
// 検索
const results = await redis.ft.search('idx:products', 'guide', {
LIMIT: { from: 0, size: 10 }
});
RedisJSON
// JSONオブジェクトを保存
await redis.json.set('user:1000', '$', {
name: 'Alice',
age: 30,
address: {
city: 'Tokyo',
country: 'Japan'
},
tags: ['developer', 'blogger']
});
// 特定のパスを取得
const name = await redis.json.get('user:1000', { path: '$.name' });
// 配列に要素追加
await redis.json.arrAppend('user:1000', '$.tags', 'writer');
// 数値を増加
await redis.json.numIncrBy('user:1000', '$.age', 1);
パフォーマンス最適化
1. パイプライン
複数のコマンドをまとめて送信し、ネットワークラウンドトリップを削減します。
const pipeline = redis.pipeline();
pipeline.set('key1', 'value1');
pipeline.set('key2', 'value2');
pipeline.get('key1');
pipeline.incr('counter');
const results = await pipeline.exec();
2. トランザクション
複数の操作をアトミックに実行します。
async function transferPoints(fromUser, toUser, points) {
const multi = redis.multi();
multi.decrBy(`user:${fromUser}:points`, points);
multi.incrBy(`user:${toUser}:points`, points);
const results = await multi.exec();
return results;
}
3. Lua スクリプト
複雑なロジックをRedis内で実行し、ネットワークオーバーヘッドを削減します。
// レート制限スクリプト
const rateLimitScript = `
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call('INCR', key)
if current == 1 then
redis.call('EXPIRE', key, window)
end
if current > limit then
return 0
else
return 1
end
`;
async function checkRateLimit(userId, limit = 100, window = 60) {
const key = `ratelimit:${userId}`;
const allowed = await redis.eval(rateLimitScript, {
keys: [key],
arguments: [limit.toString(), window.toString()]
});
return allowed === 1;
}
まとめ
Redisは単なるキャッシュではなく、多様なデータ構造とパターンを提供する強力なツールです。
重要なポイント:
- データ構造を適切に選択する(String, List, Set, Hash, Sorted Set)
- キャッシュパターンを理解し、用途に応じて使い分ける
- セッション管理やPub/Subで分散システムを構築
- パイプラインやLuaスクリプトでパフォーマンスを最適化
- Redis Stackで全文検索やJSON操作などの高度な機能を活用
Redisを正しく活用することで、アプリケーションのパフォーマンスを大幅に向上させることができます。本記事を参考に、効果的なRedis活用を実践してください。