「Redis は単なるキーバリューストアじゃない」と佐藤CTOは強調した。「データ構造サーバーであり、キャッシュ、セッションストア、メッセージブローカー、ランキングエンジンとしても使える。ただし、その特性を正しく理解しないと痛い目に遭う。」
1. Redis アーキテクチャ
シングルスレッドモデル
Redis はメインの処理がシングルスレッドで動作する。
| 特性 | 説明 |
|---|---|
| イベントループ | epoll/kqueue ベースの I/O 多重化 |
| アトミック性 | 単一コマンドはアトミックに実行 |
| ロックフリー | 競合が発生しない |
| スループット | 単一ノードで 100K+ ops/s |
| 制限 | 重い処理が全体をブロックする |
デプロイメントパターン
// 1. Standalone(単一ノード)
import Redis from 'ioredis';
const standalone = new Redis({
host: 'redis-primary',
port: 6379,
password: process.env.REDIS_PASSWORD,
db: 0,
retryStrategy: (times) => Math.min(times * 50, 2000),
});
// 2. Sentinel(高可用性)
const sentinel = new Redis({
sentinels: [
{ host: 'sentinel-1', port: 26379 },
{ host: 'sentinel-2', port: 26379 },
{ host: 'sentinel-3', port: 26379 },
],
name: 'mymaster',
password: process.env.REDIS_PASSWORD,
sentinelPassword: process.env.SENTINEL_PASSWORD,
});
// 3. Cluster(水平スケーリング)
const cluster = new Redis.Cluster([
{ host: 'redis-node-1', port: 6379 },
{ host: 'redis-node-2', port: 6379 },
{ host: 'redis-node-3', port: 6379 },
], {
redisOptions: {
password: process.env.REDIS_PASSWORD,
},
scaleReads: 'slave',
natMap: {}, // NAT環境用のマッピング
});
2. Redis データ構造の活用
class RedisPerformancePatterns {
constructor(private redis: Redis) {}
// String: シンプルなキャッシュ
async cacheApiResponse(endpoint: string, response: object, ttl: number): Promise<void> {
await this.redis.set(
`api:${endpoint}`,
JSON.stringify(response),
'EX', ttl
);
}
// Hash: オブジェクトの部分更新
async updateUserProfile(userId: string, fields: Record<string, string>): Promise<void> {
const key = `user:${userId}`;
await this.redis.hmset(key, fields);
await this.redis.expire(key, 900); // 15分
}
async getUserField(userId: string, field: string): Promise<string | null> {
return this.redis.hget(`user:${userId}`, field);
}
// Sorted Set: リアルタイムランキング
async updateLeaderboard(boardId: string, userId: string, score: number): Promise<void> {
await this.redis.zadd(`leaderboard:${boardId}`, score, userId);
}
async getTopN(boardId: string, n: number): Promise<Array<{ userId: string; score: number }>> {
const results = await this.redis.zrevrange(
`leaderboard:${boardId}`, 0, n - 1, 'WITHSCORES'
);
const entries: Array<{ userId: string; score: number }> = [];
for (let i = 0; i < results.length; i += 2) {
entries.push({ userId: results[i], score: parseFloat(results[i + 1]) });
}
return entries;
}
// HyperLogLog: ユニークカウント(近似)
async trackUniqueVisitor(page: string, visitorId: string): Promise<void> {
await this.redis.pfadd(`uv:${page}:${this.todayKey()}`, visitorId);
}
async getUniqueVisitorCount(page: string): Promise<number> {
return this.redis.pfcount(`uv:${page}:${this.todayKey()}`);
}
// Stream: イベントログ
async publishEvent(stream: string, event: Record<string, string>): Promise<string> {
return this.redis.xadd(stream, '*', ...Object.entries(event).flat());
}
// Lua スクリプト: アトミックな複合操作
async rateLimit(key: string, limit: number, windowSeconds: number): Promise<boolean> {
const script = `
local current = redis.call('INCR', KEYS[1])
if current == 1 then
redis.call('EXPIRE', KEYS[1], ARGV[1])
end
return current <= tonumber(ARGV[2])
`;
const result = await this.redis.eval(
script, 1,
`ratelimit:${key}`,
windowSeconds.toString(),
limit.toString()
);
return result === 1;
}
private todayKey(): string {
return new Date().toISOString().split('T')[0];
}
}
3. Eviction ポリシー
| ポリシー | 説明 | 適用場面 |
|---|---|---|
| noeviction | メモリ上限でエラー | データ損失不可 |
| allkeys-lru | 全キーから LRU 削除 | 汎用キャッシュ |
| allkeys-lfu | 全キーから LFU 削除 | アクセス頻度に偏りあり |
| volatile-lru | TTL付きキーから LRU | 永続キーと一時キーの混在 |
| volatile-lfu | TTL付きキーから LFU | 同上 + 頻度考慮 |
| volatile-ttl | TTL が短いキーから削除 | TTL に意味がある場合 |
| allkeys-random | ランダム削除 | アクセスパターンが均等 |
// Redis メモリ管理の監視
class RedisMemoryMonitor {
constructor(private redis: Redis) {}
async getMemoryStats(): Promise<{
usedMemory: number;
maxMemory: number;
utilizationPercent: number;
evictionPolicy: string;
evictedKeys: number;
fragmentationRatio: number;
}> {
const info = await this.redis.info('memory');
const stats = await this.redis.info('stats');
const parse = (text: string, key: string): string => {
const match = text.match(new RegExp(`${key}:(.+)`));
return match ? match[1].trim() : '0';
};
const usedMemory = parseInt(parse(info, 'used_memory'));
const maxMemory = parseInt(parse(info, 'maxmemory'));
const fragmentationRatio = parseFloat(parse(info, 'mem_fragmentation_ratio'));
const evictionPolicy = parse(info, 'maxmemory-policy');
const evictedKeys = parseInt(parse(stats, 'evicted_keys'));
return {
usedMemory,
maxMemory,
utilizationPercent: maxMemory > 0 ? (usedMemory / maxMemory) * 100 : 0,
evictionPolicy,
evictedKeys,
fragmentationRatio,
};
}
// キーの分析
async analyzeKeyDistribution(): Promise<Record<string, { count: number; memory: number }>> {
const distribution: Record<string, { count: number; memory: number }> = {};
let cursor = '0';
do {
const [nextCursor, keys] = await this.redis.scan(
cursor, 'COUNT', 1000
);
cursor = nextCursor;
for (const key of keys) {
const prefix = key.split(':')[0];
if (!distribution[prefix]) {
distribution[prefix] = { count: 0, memory: 0 };
}
distribution[prefix].count++;
const memUsage = await this.redis.memory('USAGE', key);
distribution[prefix].memory += memUsage as number;
}
} while (cursor !== '0');
return distribution;
}
}
4. Redis vs Memcached
| 項目 | Redis | Memcached |
|---|---|---|
| データ構造 | String/Hash/List/Set/SortedSet 等 | String のみ |
| 永続化 | RDB/AOF | なし |
| レプリケーション | 対応(Sentinel/Cluster) | なし(クライアント側) |
| スレッドモデル | シングルスレッド(I/O はマルチ) | マルチスレッド |
| メモリ効率 | 中程度 | 高い(slab allocator) |
| 最大値サイズ | 512MB | 1MB(デフォルト) |
| Pub/Sub | 対応 | 非対応 |
| Lua スクリプト | 対応 | 非対応 |
| 適用場面 | 多機能キャッシュ、データストア | 単純な KV キャッシュ |
コラム: Redis Cluster のハッシュスロット
Redis Cluster は 16,384 個のハッシュスロットをノードに分配する。
CRC16(key) mod 16384 → スロット番号 → 担当ノード
例:
- user:1001 → CRC16("user:1001") mod 16384 = 5649 → Node A
- user:1002 → CRC16("user:1002") mod 16384 = 12832 → Node C
ハッシュタグ: {user}:1001 と {user}:1002 は同じスロットに配置される({} 内がハッシュ対象)。これにより、関連キーを同一ノードに配置してマルチキー操作が可能になる。
// ハッシュタグを使った関連キーの配置
await redis.set('{order:123}:details', '...');
await redis.set('{order:123}:items', '...');
// 同じノードに配置されるため、MULTI/EXEC が使える
まとめ
| トピック | 要点 |
|---|---|
| Redis アーキテクチャ | シングルスレッド + I/O 多重化、100K+ ops/s |
| デプロイパターン | Standalone/Sentinel/Cluster |
| データ構造 | String/Hash/Set/SortedSet/HyperLogLog/Stream |
| Eviction | allkeys-lru/lfu が一般的、volatile-* はTTL付き限定 |
| Redis vs Memcached | Redis は多機能、Memcached はシンプル高効率 |
チェックリスト
- Redis のシングルスレッドモデルの利点と制約を説明できる
- Sentinel と Cluster の違いを理解した
- ユースケースに応じたデータ構造の選択ができる
- Eviction ポリシーの選択基準を知っている
- Redis と Memcached の使い分けを説明できる
次のステップへ
分散キャッシュの基盤を理解した。次は CDN とエッジキャッシング を学び、ユーザーに最も近い場所でのキャッシュ戦略を身につけよう。
推定読了時間: 40分