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-lruTTL付きのキーでLRU削除
allkeys-randomランダムに削除
volatile-ttlTTLが短いキーから削除

キー命名規則

# 推奨: コロン区切り
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フォーマッターが便利です。