Developer Tools

Redisはキャッシュだけじゃない:データ構造、Pub/Sub、そしてリアルタイムアプリケーション

Redisをキャッシュ以上の用途で活用する方法を学びましょう。データ構造、Pub/Subメッセージング、ストリーム、そして高速でスケーラブルなアプリケーション構築のパターンを解説します。

10分で読めます

サーバールームと輝く接続

多くの開発者はRedisを「あの速いキャッシュ」として認識しています。しかしRedisは、セッション管理、リアルタイムランキング、レート制限、メッセージキューなどをサブミリ秒のレイテンシですべて処理できる、フル機能のインメモリデータ構造サーバーです。

Redisがこれほど速い理由

Redisはすべてをメモリ上に保存し、シングルスレッドのイベントループでコマンドを処理します。読み取り時のディスクI/Oなし、ロック競合なし、コンテキストスイッチなし。その結果、一般的なハードウェアで1秒間に10万回以上の操作が可能です。

レイテンシの一般的な比較:
┌─────────────────┬──────────────┐
│ 操作            │ レイテンシ   │
├─────────────────┼──────────────┤
│ Redis GET       │ 0.1 ms       │
│ PostgreSQL SEL  │ 1-5 ms       │
│ REST API call   │ 50-200 ms    │
│ ディスク読み取り │ 5-10 ms      │
└─────────────────┴──────────────┘

コアデータ構造

Redisは単なるキーバリューストアではありません。アプリケーションの一般的なニーズに直接対応できるリッチなデータ構造をサポートしています。

Strings — 基本の構成要素

Stringsはテキスト、数値、またはバイナリデータを最大512 MBまで保持します。

SET user:1001:name "Alice"
GET user:1001:name          # "Alice"

# アトミックカウンター
INCR page:views             # 1, 2, 3, ...
INCRBY cart:total 2500      # 25.00を加算(センt単位で保存)

# 有効期限付きキー(TTL)
SET session:abc123 "{...}" EX 3600   # 1時間後に期限切れ
TTL session:abc123                    # 残り秒数

Hashes — 軽量オブジェクト

Hashesはシリアライズのオーバーヘッドなしにオブジェクトを保存するのに最適です。

HSET user:1001 name "Alice" email "alice@example.com" plan "pro"
HGET user:1001 name         # "Alice"
HGETALL user:1001           # すべてのフィールドと値
HINCRBY user:1001 logins 1  # フィールドのアトミックインクリメント

Hashesは、各フィールドを個別のキーとして保存するより10倍少ないメモリを使用します。

Lists — キューとフィード

両端からのプッシュ/ポップをサポートする順序付きコレクション:

LPUSH notifications:alice "New comment on your post"
LPUSH notifications:alice "You have a new follower"
LRANGE notifications:alice 0 9    # 最新10件の通知

# キューとして使用(プロデューサー/コンシューマー)
RPUSH queue:emails "send-welcome"
LPOP queue:emails                  # 次のジョブを処理

Sets — 一意のコレクション

ユニークな文字列の順序なしコレクション:

SADD tags:post:42 "redis" "database" "nosql"
SMEMBERS tags:post:42             # すべてのタグ
SISMEMBER tags:post:42 "redis"    # true

# Set操作
SINTER tags:post:42 tags:post:99  # 共通タグ
SUNION tags:post:42 tags:post:99  # 全タグの合算

Sorted Sets — ランキングと順位表

各メンバーにスコアを持ち、自動的にソートされるSet:

ZADD leaderboard 1500 "alice" 2300 "bob" 1800 "charlie"
ZREVRANGE leaderboard 0 2 WITHSCORES   # 上位3プレイヤー
ZRANK leaderboard "alice"               # 順位(0始まり)
ZINCRBY leaderboard 100 "alice"         # Aliceが100ポイント獲得

これが、ゲームのランキング、トレンド投稿、優先度キューを大規模に構築する方法です。

実践的なパターン

パターン1: セッションストレージ

データベースベースのセッションの代わりに、即時ルックアップのためにRedisを使用する:

import redis
r = redis.Redis()

# セッションを保存
r.setex(f"session:{token}", 3600, json.dumps(user_data))

# セッションを取得
data = r.get(f"session:{token}")

なぜ? データベースセッションはリクエストごとに1〜5msを追加します。Redisセッションは0.1msです。毎秒1,000リクエストでは、1秒ごとに5秒分の時間を節約できます。

パターン2: レート制限

3つのコマンドによるスライディングウィンドウ方式のレートリミッター:

# ユーザーごとに1分間に100リクエストまで許可
MULTI
INCR rate:user:1001
EXPIRE rate:user:1001 60
EXEC

# 確認:INCRの結果が100を超えたらリクエストを拒否

パターン3: キャッシュアサイドによるキャッシング

最も一般的なキャッシングパターン:

def get_user(user_id):
    # 1. キャッシュを確認
    cached = redis.get(f"user:{user_id}")
    if cached:
        return json.loads(cached)
    
    # 2. キャッシュミス → データベースを照会
    user = db.query("SELECT * FROM users WHERE id = %s", user_id)
    
    # 3. キャッシュに保存(5分で期限切れ)
    redis.setex(f"user:{user_id}", 300, json.dumps(user))
    return user

パターン4: リアルタイムイベントのためのPub/Sub

Redis Pub/Subはサービス間のリアルタイムメッセージングを可能にします:

# サブスクライバー(メッセージをリッスン)
SUBSCRIBE chat:room:42

# パブリッシャー(メッセージを送信)
PUBLISH chat:room:42 "Hello everyone!"

アプリケーションコードでは:

# パブリッシャー
redis.publish("notifications", json.dumps({
    "type": "new_order",
    "order_id": 12345
}))

# サブスクライバー
pubsub = redis.pubsub()
pubsub.subscribe("notifications")
for message in pubsub.listen():
    handle_notification(message)

パターン5: 分散ロック

複数のサーバー間での競合状態を防止する:

# ロックを取得(NX = 存在しない場合のみ、EX = 期限切れ)
SET lock:invoice:gen "worker-1" NX EX 30

# ロックを解放(所有者の場合のみ — Luaスクリプトを使用)

Redis Streams — イベントソーシングとメッセージキュー

StreamsはKafkaスタイルのイベントログに対するRedisの回答です:

# イベントを追加
XADD orders * user_id 1001 product "Widget" qty 3
XADD orders * user_id 1002 product "Gadget" qty 1

# 最新のイベントを読み取る
XRANGE orders - + COUNT 10

# コンシューマーグループ(複数のワーカー)
XGROUP CREATE orders processors $ MKSTREAM
XREADGROUP GROUP processors worker-1 COUNT 5 BLOCK 2000 STREAMS orders >

Streamsはコンシューマーグループを持つ永続的で再生可能なイベントログを提供します。マイクロサービス間の通信に最適です。

パフォーマンスのヒント

1. 大量操作にはパイプラインを使用する

100回のラウンドトリップの代わりに、100個のコマンドを一度に送信:

pipe = redis.pipeline()
for i in range(100):
    pipe.set(f"key:{i}", f"value:{i}")
pipe.execute()  # シングルラウンドトリップ

2. 適切なデータ構造を選ぶ

  • カウンターが必要? → INCR(GET + SETではなく)
  • オブジェクトが必要? → Hash(シリアライズされたJSON文字列ではなく)
  • ユニークなアイテムが必要? → Set(List + 重複排除ロジックではなく)
  • ランキングが必要? → Sorted Set(クエリ後にSortするのではなく)

3. すべてにTTLを設定する

メモリは有限です。キャッシュキーには常に有効期限を設定しましょう:

SET cache:api:result "{...}" EX 300   # 5分のTTL

4. キーの命名規則を使用する

resource:id:field
user:1001:profile
cache:api:v2:products
session:abc123
queue:emails:pending

永続化:RDB vs AOF

Redisは耐久性のためにデータをディスクに保存できます:

モード 仕組み トレードオフ
RDB ポイントインタイムスナップショット 高速リカバリ、データ損失の可能性あり
AOF すべての書き込み操作をログに記録 低速、データ損失は最小限
両方 RDB + AOFの組み合わせ 最高の耐久性
# redis.confの設定
save 900 1          # 900秒以内に1キーが変更された場合にスナップショット
appendonly yes      # AOFを有効化
appendfsync everysec  # 毎秒Fsync

クイックコマンドリファレンス

特定のRedisコマンドを調べたいときは、Redis Cheat Sheet をご利用ください。200以上のコマンドをデータ構造別に整理し、構文、使用例、ワンクリックコピー機能付きで掲載しています。

まとめ

Redisはキャッシュをはるかに超えた存在です。そのデータ構造は実際のアプリケーションのニーズに明確に対応しています:

  • Strings → セッション、カウンター、シンプルなキャッシュ
  • Hashes → ユーザープロファイル、設定オブジェクト
  • Lists → キュー、アクティビティフィード、最近のアイテム
  • Sets → タグ、ユニーク訪問者、リレーションシップ
  • Sorted Sets → ランキング、順位表、優先度キュー
  • Streams → イベントソーシング、メッセージキュー
  • Pub/Sub → リアルタイム通知、チャット

まずはキャッシングから始めて、そこで止まらないでください。Redisはスタック内のいくつかのインフラコンポーネントを置き換えることができます。そしてすべてをサブミリ秒のスピードでこなします。