LabHub

ブログ

Redis 完全ガイド 2025:キャッシング戦略、データ構造、Pub/Sub、Redis Stackまで

한국어English日本語

はじめに

Redisは2025年現在、最も広く使われているインメモリデータストアです。単純なキャッシュを超えて、メッセージブローカー、セッションストア、リアルタイムリーダーボード、Rate Limiter、分散ロックまで多様な役割を果たします。Redis 7.4の新機能とRedis Stackの登場、そしてValkeyフォーク論争まで — 本記事ではRedisの全てを網羅します。


1. Redis 概要

Redisとは?

Redis(Remote Dictionary Server)はインメモリキーバリューデータストアです。全てのデータをメモリに保存するため、マイクロ秒単位の応答時間を提供します。

Redis 7.4 主要機能

Valkey フォークの経緯

2024年にRedisがライセンスを変更し(SSPL + RSALv2)、Linux FoundationがRedis 7.2をベースにValkeyをフォークしました。AWS、Google、Oracleなどがサポートしています。現在は互換性を維持していますが、長期的には分岐する可能性があります。


2. 基本データ構造 5つ

2.1 String

最も基本的なデータ型。最大512MBまで保存可能です。

# 基本 SET/GET
SET user:1:name "Alice"
GET user:1:name

# アトミックなインクリメント/デクリメント
SET page:views 0
INCR page:views          # 1
INCRBY page:views 10     # 11

# TTL設定
SET session:abc123 "user_data" EX 3600   # 1時間後に有効期限切れ
TTL session:abc123                        # 残り時間確認

# SETオプション
SET lock:resource "owner1" NX EX 30      # NX: キーが存在しない場合のみ設定
SET user:1:name "Bob" XX                 # XX: キーが存在する場合のみ更新

活用事例: セッショントークン、カウンター、一時データ、分散ロック

2.2 List

双方向連結リスト。両端からO(1)でpush/pop可能です。

# 基本操作
LPUSH queue:emails "email1" "email2" "email3"
RPOP queue:emails                        # "email1" (FIFOキュー)

# 範囲取得
LRANGE queue:emails 0 -1                 # 全要素

# ブロッキングポップ(メッセージキューとして活用)
BRPOP queue:emails 30                    # 30秒待機後ポップ

# トリミング(最新N件を保持)
LPUSH notifications:user1 "new_msg"
LTRIM notifications:user1 0 99          # 最新100件のみ保持

活用事例: メッセージキュー、最近のアクティビティフィード、ジョブキュー

2.3 Set

順序なしのユニーク要素の集合。集合演算(和集合、積集合、差集合)をサポートします。

# 基本操作
SADD tags:post:1 "python" "redis" "backend"
SADD tags:post:2 "python" "django" "orm"

# メンバーシップ確認
SISMEMBER tags:post:1 "python"           # 1 (true)

# 集合演算
SINTER tags:post:1 tags:post:2           # "python" (積集合)
SUNION tags:post:1 tags:post:2           # 和集合
SDIFF tags:post:1 tags:post:2            # post:1にのみある要素

# ランダム抽出
SRANDMEMBER tags:post:1 2               # ランダム2個

活用事例: タグシステム、ユニーク訪問者追跡、フレンド関係、レコメンドシステム

2.4 Sorted Set (ZSet)

スコアでソートされたユニーク要素の集合。リーダーボードに最適です。

# リーダーボード実装
ZADD leaderboard 1500 "player:alice"
ZADD leaderboard 2300 "player:bob"
ZADD leaderboard 1800 "player:charlie"

# ランキング取得(スコア降順)
ZREVRANGE leaderboard 0 2 WITHSCORES
# 1) "player:bob"     2) "2300"
# 3) "player:charlie" 4) "1800"
# 5) "player:alice"   6) "1500"

# 特定メンバーのランク(0始まり)
ZREVRANK leaderboard "player:alice"      # 2

# スコア増加
ZINCRBY leaderboard 500 "player:alice"   # 2000

# 範囲検索
ZRANGEBYSCORE leaderboard 1500 2000 WITHSCORES

活用事例: リーダーボード、優先度キュー、時間順イベント、Rate Limiting

2.5 Hash

フィールドと値のペアのマップ。オブジェクトの表現に適しています。

# ユーザープロフィール保存
HSET user:1 name "Alice" email "alice@example.com" age "30" role "admin"

# 個別フィールド取得
HGET user:1 name                         # "Alice"

# 全体取得
HGETALL user:1

# フィールドのインクリメント
HINCRBY user:1 age 1                     # 31

# 存在確認
HEXISTS user:1 email                     # 1 (true)

# 複数フィールドを一度に
HMGET user:1 name email role

活用事例: ユーザープロフィール、設定値、セッションデータ、ショッピングカート


3. 高度なデータ構造

3.1 HyperLogLog

大量のユニーク要素数を推定する確率的データ構造。12KBのメモリで最大2の64乗個をカウント可能(誤差率0.81%)。

# ユニーク訪問者数の推定
PFADD visitors:2025-03-23 "user1" "user2" "user3"
PFADD visitors:2025-03-23 "user1" "user4"          # user1は重複

PFCOUNT visitors:2025-03-23                         # 4

# 複数日のマージ
PFMERGE visitors:week visitors:2025-03-23 visitors:2025-03-24
PFCOUNT visitors:week

3.2 Bitmap

ビット単位の操作。ブーリアン状態の追跡にメモリ効率的です。

# 日次出席チェック
SETBIT attendance:2025-03-23 1001 1      # ユーザー1001出席
SETBIT attendance:2025-03-23 1002 1
SETBIT attendance:2025-03-23 1003 0      # 欠席

# 出席確認
GETBIT attendance:2025-03-23 1001        # 1

# 出席人数
BITCOUNT attendance:2025-03-23           # 2

# 連続出席(AND演算)
BITOP AND consecutive attendance:2025-03-22 attendance:2025-03-23
BITCOUNT consecutive

3.3 Geospatial

位置ベースのデータ。半径検索、距離計算をサポートします。

# 位置追加(経度、緯度)
GEOADD stores 139.7671 35.6812 "tokyo-store"
GEOADD stores 135.5023 34.6937 "osaka-store"
GEOADD stores 136.9066 35.1815 "nagoya-store"

# 距離計算
GEODIST stores "tokyo-store" "osaka-store" km

# 半径検索(Redis 6.2+)
GEOSEARCH stores FROMLONLAT 139.7671 35.6812 BYRADIUS 500 km ASC COUNT 5

3.4 Redis Streams

ログベースのメッセージ構造。KafkaのようにConsumer Groupをサポートします。

# メッセージ追加
XADD events * type "order" user_id "123" amount "50000"
XADD events * type "payment" user_id "123" status "completed"

# 読み取り
XRANGE events - + COUNT 10

# Consumer Group作成
XGROUP CREATE events analytics-group $ MKSTREAM

# Consumerとして読み取り
XREADGROUP GROUP analytics-group consumer-1 COUNT 5 BLOCK 2000 STREAMS events >

# ACK(処理完了)
XACK events analytics-group "1679000000000-0"

# 未処理メッセージ確認
XPENDING events analytics-group

4. キャッシングパターン

4.1 Cache-Aside(Lazy Loading)

最も一般的なパターン。アプリケーションがキャッシュを直接管理します。

import redis
import json

r = redis.Redis(host="localhost", port=6379, decode_responses=True)

def get_user(user_id: int) -> dict:
    cache_key = f"user:{user_id}"

    # 1. キャッシュ確認
    cached = r.get(cache_key)
    if cached:
        return json.loads(cached)

    # 2. キャッシュミス -> DB問合せ
    user = db.query(User).filter(User.id == user_id).first()
    if not user:
        return None

    # 3. キャッシュに保存
    r.setex(cache_key, 3600, json.dumps(user.to_dict()))
    return user.to_dict()

def update_user(user_id: int, data: dict):
    db.query(User).filter(User.id == user_id).update(data)
    db.commit()

    # キャッシュ無効化
    r.delete(f"user:{user_id}")

4.2 Write-Through

書き込み時にキャッシュとDBを同時に更新します。

def save_user(user_id: int, data: dict):
    cache_key = f"user:{user_id}"

    # DBとキャッシュを同時更新
    db.query(User).filter(User.id == user_id).update(data)
    db.commit()

    r.setex(cache_key, 3600, json.dumps(data))

4.3 Write-Behind(Write-Back)

キャッシュに先に書き込み、非同期でDBに反映します。

def save_user_async(user_id: int, data: dict):
    cache_key = f"user:{user_id}"

    # キャッシュに先に書き込み
    r.setex(cache_key, 3600, json.dumps(data))

    # 非同期でDBに反映(Celery等)
    sync_to_db_task.delay(user_id, data)

4.4 キャッシングパターン比較

パターン読取性能書込性能一貫性複雑度
Cache-Aside高い普通結果整合性低い
Write-Through高い低い強い一貫性中程度
Write-Behind高い高い結果整合性高い
Read-Through高い普通結果整合性中程度

5. キャッシュ無効化

5.1 TTLベース

最もシンプルな方法。一定時間後に自動削除されます。

SET product:123 "data" EX 300           # 5分後に有効期限切れ

5.2 イベントベースの無効化

データ変更時に明示的にキャッシュを削除します。

def update_product(product_id: int, data: dict):
    db.update(product_id, data)

    # 関連キャッシュを全て削除
    r.delete(f"product:{product_id}")
    r.delete(f"product_list:category:{data['category_id']}")
    r.delete("product_list:featured")

5.3 バージョンキー

キーにバージョンを含めて一括無効化します。

def get_product_list(category_id: int) -> list:
    version = r.get(f"product_version:{category_id}") or "1"
    cache_key = f"products:cat:{category_id}:v:{version}"

    cached = r.get(cache_key)
    if cached:
        return json.loads(cached)

    products = db.query(Product).filter(Product.category_id == category_id).all()
    r.setex(cache_key, 3600, json.dumps([p.to_dict() for p in products]))
    return [p.to_dict() for p in products]

def invalidate_category(category_id: int):
    # バージョンを増やして既存キャッシュを無効化
    r.incr(f"product_version:{category_id}")

5.4 Thundering Herd 防止

キャッシュ有効期限切れ時に同時に多数のリクエストがDBに殺到する問題を防止します。

import random

def get_with_jitter(key: str, ttl: int = 3600) -> dict:
    cached = r.get(key)
    if cached:
        return json.loads(cached)

    # 分散ロックで1つのリクエストだけがDB問合せ
    lock_key = f"lock:{key}"
    if r.set(lock_key, "1", nx=True, ex=10):
        try:
            data = fetch_from_db(key)
            # TTLにランダムジッターを追加
            jitter = random.randint(0, 300)
            r.setex(key, ttl + jitter, json.dumps(data))
            return data
        finally:
            r.delete(lock_key)
    else:
        import time
        time.sleep(0.1)
        return get_with_jitter(key, ttl)

6. Pub/SubとStreams

6.1 Pub/Sub 基本

# 購読者
SUBSCRIBE notifications:user:123

# 発行者
PUBLISH notifications:user:123 "You have a new message!"

# パターン購読
PSUBSCRIBE notifications:*
# Python購読者
import redis

r = redis.Redis()
pubsub = r.pubsub()
pubsub.subscribe("notifications:user:123")

for message in pubsub.listen():
    if message["type"] == "message":
        print(f"Received: {message['data']}")

6.2 Redis Streams vs Kafka

機能Redis StreamsKafka
メッセージ永続性メモリ + AOFディスク
スループット数万/秒数十万/秒
Consumer Groupサポートサポート
メッセージリプレイサポートサポート
パーティショニング非サポートサポート
運用の複雑さ低い高い
適切な規模中小大規模

6.3 Streamsを活用したイベントドリブンパターン

import redis

r = redis.Redis(decode_responses=True)

# イベント発行
def publish_event(stream: str, event_type: str, data: dict):
    r.xadd(stream, {"type": event_type, **data}, maxlen=10000)

# Consumer Groupベースの処理
def consume_events(stream: str, group: str, consumer: str):
    try:
        r.xgroup_create(stream, group, id="0", mkstream=True)
    except redis.ResponseError:
        pass

    while True:
        messages = r.xreadgroup(
            group, consumer,
            {stream: ">"},
            count=10,
            block=5000,
        )

        for stream_name, entries in messages:
            for msg_id, fields in entries:
                try:
                    process_event(fields)
                    r.xack(stream_name, group, msg_id)
                except Exception as e:
                    print(f"Error processing {msg_id}: {e}")

7. Redis Stack

Redis StackはRedisにJSON、Search、TimeSeries、Bloom Filterモジュールを追加した拡張版です。

7.1 RedisJSON

# JSONドキュメント保存
JSON.SET user:1 $ '{"name":"Alice","age":30,"address":{"city":"Tokyo","zip":"100-0001"},"tags":["python","redis"]}'

# パスベースのクエリ
JSON.GET user:1 $.name                   # "Alice"
JSON.GET user:1 $.address.city           # "Tokyo"

# 部分更新
JSON.SET user:1 $.age 31
JSON.ARRAPPEND user:1 $.tags '"fastapi"'

# 数値インクリメント
JSON.NUMINCRBY user:1 $.age 1

7.2 RediSearch(全文検索)

# インデックス作成
FT.CREATE idx:products
  ON JSON
  PREFIX 1 product:
  SCHEMA
    $.name AS name TEXT WEIGHT 5.0
    $.description AS description TEXT
    $.price AS price NUMERIC SORTABLE
    $.category AS category TAG

# ドキュメント追加
JSON.SET product:1 $ '{"name":"Redis in Action","description":"Complete guide to Redis","price":45000,"category":"book"}'
JSON.SET product:2 $ '{"name":"Python Cookbook","description":"Python recipes and patterns","price":38000,"category":"book"}'

# 検索
FT.SEARCH idx:products "Redis guide"
FT.SEARCH idx:products "@category:{book} @price:[30000 50000]"
FT.SEARCH idx:products "@name:(Python)" SORTBY price ASC

7.3 RedisTimeSeries

# 時系列データ作成
TS.CREATE sensor:temperature:1 RETENTION 86400000 LABELS sensor_id 1 type temperature

# データ追加
TS.ADD sensor:temperature:1 * 23.5
TS.ADD sensor:temperature:1 * 24.1
TS.ADD sensor:temperature:1 * 22.8

# 範囲クエリ
TS.RANGE sensor:temperature:1 - + COUNT 10

# 集約(5分平均)
TS.RANGE sensor:temperature:1 - + AGGREGATION avg 300000

8. Luaスクリプティング

8.1 基本Luaスクリプト

# アトミックな読取-変更-書込
EVAL "
  local current = redis.call('GET', KEYS[1])
  if current then
    local new_val = tonumber(current) + tonumber(ARGV[1])
    redis.call('SET', KEYS[1], new_val)
    return new_val
  end
  return nil
" 1 counter 5

8.2 Rate Limiter(Sliding Window)

RATE_LIMIT_SCRIPT = """
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

-- ウィンドウ外の古いリクエストを削除
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)

-- 現在のリクエスト数を確認
local count = redis.call('ZCARD', key)

if count < limit then
    -- 許可:新しいリクエストを追加
    redis.call('ZADD', key, now, now .. ':' .. math.random())
    redis.call('EXPIRE', key, window)
    return 1
else
    -- 拒否
    return 0
end
"""

import redis
import time

r = redis.Redis()
rate_limit_sha = r.script_load(RATE_LIMIT_SCRIPT)

def is_allowed(user_id: str, limit: int = 100, window: int = 60) -> bool:
    key = f"ratelimit:{user_id}"
    now = int(time.time() * 1000)
    result = r.evalsha(rate_limit_sha, 1, key, limit, window * 1000, now)
    return bool(result)

8.3 分散ロック(Redlockアルゴリズム)

import redis
import uuid

class DistributedLock:
    def __init__(self, redis_client: redis.Redis, resource: str, ttl: int = 10):
        self.redis = redis_client
        self.resource = f"lock:{resource}"
        self.ttl = ttl
        self.token = str(uuid.uuid4())

    def acquire(self) -> bool:
        return bool(self.redis.set(
            self.resource, self.token,
            nx=True, ex=self.ttl,
        ))

    def release(self) -> bool:
        # Luaスクリプトでアトミックな確認+削除
        script = """
        if redis.call('GET', KEYS[1]) == ARGV[1] then
            return redis.call('DEL', KEYS[1])
        end
        return 0
        """
        return bool(self.redis.eval(script, 1, self.resource, self.token))

    def __enter__(self):
        if not self.acquire():
            raise Exception("Could not acquire lock")
        return self

    def __exit__(self, *args):
        self.release()

# 使用例
r = redis.Redis()
with DistributedLock(r, "order:process:123"):
    # クリティカルセクション — 1つのプロセスだけが実行
    process_order(123)

9. Redis Cluster

9.1 ハッシュスロット

Redis Clusterは16384個のハッシュスロットでデータを分散します。キーのCRC16ハッシュ値を16384で割った余りがスロット番号です。

ノード構成例:
  Node A: スロット 0-5460
  Node B: スロット 5461-10922
  Node C: スロット 10923-16383

各ノードに1つのレプリカを追加すると:
  Node A -> Replica A'
  Node B -> Replica B'
  Node C -> Replica C'

9.2 クラスタ設定

# クラスタ作成(6ノード:3マスター + 3レプリカ)
redis-cli --cluster create \
  127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 \
  127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7006 \
  --cluster-replicas 1

# クラスタステータス確認
redis-cli -c -p 7001 cluster info
redis-cli -c -p 7001 cluster nodes

9.3 フェイルオーバーとリシャーディング

# 手動フェイルオーバー
redis-cli -c -p 7004 cluster failover

# リシャーディング(スロット移動)
redis-cli --cluster reshard 127.0.0.1:7001

# ノード追加
redis-cli --cluster add-node 127.0.0.1:7007 127.0.0.1:7001

9.4 ハッシュタグ

同じスロットにキーを配置してマルチキー操作を可能にします。

# ハッシュタグ使用 — 中括弧内の部分でスロット決定
SET {user:1}:profile "data"
SET {user:1}:settings "data"
SET {user:1}:sessions "data"

# この3つのキーは同じスロットに配置 -> マルチキー操作が可能

10. Redis 実践活用

10.1 セッションストア

import redis
import json
import uuid

r = redis.Redis(decode_responses=True)

def create_session(user_id: int, data: dict) -> str:
    session_id = str(uuid.uuid4())
    session_data = {
        "user_id": str(user_id),
        "created_at": str(time.time()),
        **data,
    }
    r.hset(f"session:{session_id}", mapping=session_data)
    r.expire(f"session:{session_id}", 86400)  # 24時間
    return session_id

def get_session(session_id: str) -> dict | None:
    data = r.hgetall(f"session:{session_id}")
    if not data:
        return None
    # アクセス時にTTLを更新
    r.expire(f"session:{session_id}", 86400)
    return data

def destroy_session(session_id: str):
    r.delete(f"session:{session_id}")

10.2 Rate Limiter(Fixed Window)

def check_rate_limit(user_id: str, limit: int = 100, window: int = 60) -> bool:
    key = f"rate:{user_id}:{int(time.time()) // window}"

    pipe = r.pipeline()
    pipe.incr(key)
    pipe.expire(key, window)
    count, _ = pipe.execute()

    return count <= limit

10.3 リーダーボード

class Leaderboard:
    def __init__(self, name: str):
        self.key = f"leaderboard:{name}"

    def add_score(self, player_id: str, score: float):
        r.zadd(self.key, {player_id: score})

    def increment_score(self, player_id: str, delta: float):
        r.zincrby(self.key, delta, player_id)

    def get_rank(self, player_id: str) -> int | None:
        rank = r.zrevrank(self.key, player_id)
        return rank + 1 if rank is not None else None

    def get_top(self, count: int = 10) -> list[tuple[str, float]]:
        return r.zrevrange(self.key, 0, count - 1, withscores=True)

# 使用例
lb = Leaderboard("weekly")
lb.add_score("player:alice", 1500)
lb.increment_score("player:alice", 200)
print(lb.get_top(10))

10.4 シンプルジョブキュー

import json
import time

def enqueue_job(queue: str, job_data: dict):
    job = {
        "id": str(uuid.uuid4()),
        "data": job_data,
        "created_at": time.time(),
    }
    r.lpush(f"queue:{queue}", json.dumps(job))

def dequeue_job(queue: str, timeout: int = 30) -> dict | None:
    result = r.brpop(f"queue:{queue}", timeout=timeout)
    if result:
        _, job_json = result
        return json.loads(job_json)
    return None

def worker(queue: str):
    while True:
        job = dequeue_job(queue)
        if job:
            try:
                process_job(job["data"])
            except Exception as e:
                r.lpush(f"queue:{queue}:failed", json.dumps(job))

11. Redis vs Memcached vs DragonflyDB

機能RedisMemcachedDragonflyDB
データ構造豊富(String、List、Set等)StringのみRedis互換
永続性RDB + AOFなしスナップショット
クラスタリングRedis Clusterクライアント側シングルノード(マルチスレッド)
マルチスレッドシングルスレッド(I/Oスレッド)マルチスレッドマルチスレッド
メモリ効率普通高い高い
Pub/Subサポート非サポートサポート
Luaスクリプトサポート非サポートサポート
スループット~100K ops/s~100K ops/s~400K ops/s
適切な用途汎用シンプルキャッシュ高性能シングルノード

12. クライアントライブラリコード例

12.1 Spring Data Redis(Java)

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
}

@Service
public class UserCacheService {
    private final RedisTemplate<String, Object> redisTemplate;
    private final Duration TTL = Duration.ofHours(1);

    public void cacheUser(String userId, UserDto user) {
        String key = "user:" + userId;
        redisTemplate.opsForValue().set(key, user, TTL);
    }

    public UserDto getCachedUser(String userId) {
        String key = "user:" + userId;
        return (UserDto) redisTemplate.opsForValue().get(key);
    }
}

12.2 ioredis(Node.js)

import Redis from 'ioredis';

const redis = new Redis({
  host: 'localhost',
  port: 6379,
  retryStrategy: (times) => Math.min(times * 50, 2000),
});

// 基本キャッシング
async function getUser(userId) {
  const cacheKey = `user:${userId}`;

  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  const user = await db.findUser(userId);
  if (user) {
    await redis.setex(cacheKey, 3600, JSON.stringify(user));
  }
  return user;
}

// パイプライン(バッチ処理)
async function getMultipleUsers(userIds) {
  const pipeline = redis.pipeline();
  userIds.forEach(id => pipeline.get(`user:${id}`));
  const results = await pipeline.exec();
  return results.map(([err, val]) => val ? JSON.parse(val) : null);
}

12.3 redis-py(Python)

import redis.asyncio as aioredis
import json
import functools

pool = aioredis.ConnectionPool.from_url(
    "redis://localhost:6379",
    max_connections=20,
    decode_responses=True,
)
r = aioredis.Redis(connection_pool=pool)

# キャッシュデコレータ
def redis_cache(ttl: int = 3600):
    def decorator(func):
        @functools.wraps(func)
        async def wrapper(*args, **kwargs):
            cache_key = f"cache:{func.__name__}:{args}:{kwargs}"
            cached = await r.get(cache_key)
            if cached:
                return json.loads(cached)
            result = await func(*args, **kwargs)
            await r.setex(cache_key, ttl, json.dumps(result))
            return result
        return wrapper
    return decorator

@redis_cache(ttl=600)
async def get_product_list(category_id: int):
    return await db.get_products(category_id)

13. 面接質問 15選

Q1. Redisはシングルスレッドなのになぜ速いのですか?

Redisはイベントループベースのシングルスレッドで動作し、全データがメモリにあるためディスクI/Oがありません。epoll/kqueueベースのI/O多重化で数千の接続を効率的に処理します。Redis 6.0からI/Oスレッドをサポートし、ネットワーク処理を並列化します。

Q2. Cache-AsideとWrite-Throughの違いは?

Cache-Asideはアプリケーションがキャッシュを直接管理します(読取時にキャッシュミスならDB問合せ後キャッシュ保存)。Write-Throughは書込時にキャッシュとDBを同時更新します。Cache-Asideは実装が簡単で、Write-Throughはデータ一貫性が強いです。

Q3. Redisの永続化方式(RDB、AOF)を説明してください。

RDB(Redis Database)は特定時点のスナップショットをディスクに保存します。AOF(Append Only File)は全ての書込操作をログに記録します。RDBは高速リカバリに適し、AOFはデータ損失を最小化します。両方の使用が推奨されます。

Q4. Redis ClusterでMGETが失敗する理由は?

Redis Clusterではキーがハッシュスロットに分散されます。MGETのキーが異なるスロットにある場合、単一コマンドで処理できません。ハッシュタグ(中括弧)を使用して同じスロットにキーを配置するか、クライアントで複数リクエストに分割する必要があります。

Q5. Thundering Herd問題とは?

キャッシュキーの有効期限が切れた瞬間に多数のリクエストが同時にDBに殺到する現象です。解決法:分散ロックで1つのリクエストだけDBアクセスを許可、TTLにランダムジッター追加、バックグラウンドでのプロアクティブキャッシュ更新。

Q6. Redis Pub/SubとStreamsの違いは?

Pub/SubはFire-and-Forget方式で、購読者がいなければメッセージが消失します。Streamsはメッセージを永続保存し、Consumer Groupで安定的なメッセージ処理を保証します。StreamsはACK、再処理、履歴参照が可能です。

Q7. Redisで大きなキー(Big Key)が問題になる理由は?

大きなキーは削除時にRedisをブロッキングし、ネットワーク帯域幅を消費し、クラスタでデータ偏りを引き起こします。UNLINKコマンドで非同期削除し、大きなハッシュを複数の小さなハッシュに分割するのが良いです。

Q8. Redisのメモリポリシー(eviction policy)を説明してください。

maxmemoryに達するとevictionポリシーに従ってキーを削除します。noeviction(新規書込拒否)、allkeys-lru(LRU)、allkeys-lfu(LFU)、volatile-lru(TTL付きキーのLRU)、volatile-ttl(有効期限が近いキー優先)などがあります。

Q9. Luaスクリプトを使用する理由は?

Redisで複数コマンドをアトミックに実行するためです。ネットワークラウンドトリップを削減し、サーバー側で複雑なロジックをアトミックに実行します。代表的な事例:Rate Limiter、分散ロック解除、条件付き更新。

Q10. Redis SentinelとRedis Clusterの違いは?

Sentinelはマスター・スレーブ構造の高可用性ソリューションです(自動フェイルオーバー)。Clusterはデータを複数ノードに分散する水平スケーリングソリューションです。小規模にはSentinel、大規模データにはClusterが適しています。

Q11. Redisのパイプラインとは?

複数コマンドを一度にサーバーに送り、レスポンスを一括で受け取る技法です。ネットワークラウンドトリップ回数を大幅に削減してパフォーマンスを向上させます。100個のコマンドを個別に送ると100回ラウンドトリップしますが、パイプラインは1回です。

Q12. Redisでキーの有効期限処理はどう動作しますか?

2つのメカニズムを組み合わせます。Lazy expirationはキーアクセス時に有効期限を確認します。Active expirationは100msごとにランダムな期限切れキーをサンプリングして削除します。この組み合わせにより期限切れキーがメモリを過度に占有するのを防ぎます。

Q13. Redlockアルゴリズムとは?

Martin Kleppmannが批判しSalvatore Sanfilippoが提案した分散ロックアルゴリズムです。N個の独立したRedisインスタンスで過半数(N/2+1)で正常にロックを取得すればロックが有効です。シングルインスタンスロックより安全ですが、完璧ではありません。

Q14. Redisのslow logとは?

実行時間が特定のしきい値を超えるコマンドを記録します。slowlog-log-slower-thanでしきい値(マイクロ秒)を設定します。SLOWLOG GETで遅いコマンドを確認して最適化できます。

Q15. Redisをセッションストアとして使用する際のメリットとデメリットは?

メリット:高速な読書き、TTL自動期限切れ、水平スケーリング可能、サーバー間セッション共有。デメリット:メモリコスト、Redis障害時のセッション喪失リスク、ネットワーク依存性。永続性(AOF)とレプリケーションを有効にしてリスクを軽減できます。


14. クイズ

Q1. Redis SETコマンドのNXとXXオプションの違いは?

NX(Not eXists)はキーが存在しない場合のみ設定します。分散ロック取得に使用されます。XX(eXists)はキーが既に存在する場合のみ更新します。既存の値を安全に更新する時に使用します。

Q2. ZADDの時間計算量は何で、なぜそうなりますか?

ZADDの時間計算量はO(log N)です。Sorted Setは内部的にSkip Listを使用してソート状態を維持しており、Skip Listの挿入操作がO(log N)だからです。

Q3. KEYSコマンドをプロダクションで使ってはいけない理由は?

KEYSは全キーを走査するO(N)操作で、キーが多いとRedisを長時間ブロッキングします。代わりにSCANコマンドを使用すべきです。SCANはカーソルベースで段階的に走査し、ブロッキングを防止します。

Q4. RedisのWATCH/MULTI/EXECはどのような問題を解決しますか?

楽観的ロック(Optimistic Locking)を実装します。WATCHでキーを監視し、MULTIでトランザクションを開始し、EXECで実行します。WATCHしたキーが他のクライアントによって変更された場合、トランザクションが失敗します。競合が少ない状況で効率的です。

Q5. HyperLogLogの誤差率は約0.81%です。なぜ正確でないデータ構造を使うのですか?

正確なユニークカウントにはO(N)メモリが必要です(全要素を保存)。HyperLogLogはどのサイズの集合でも12KB固定メモリだけを使用します。1億人のユニーク訪問者を追跡する場合、Setは数GBが必要ですが、HyperLogLogは12KBで十分です。大半の分析ケースで0.81%の誤差は許容可能です。


参考資料

  1. Redis 公式ドキュメント
  2. Redis University
  3. Redis Stack ドキュメント
  4. Valkey プロジェクト
  5. Redis in Action (Manning)
  6. ioredis GitHub
  7. redis-py ドキュメント
  8. Spring Data Redis
  9. Redis ベストプラクティス
  10. Redlock アルゴリズム
  11. Martin Kleppmannの Redlock分析
  12. Redis Cluster チュートリアル
  13. DragonflyDB
  14. Redis Streams ガイド
  15. RediSearch ドキュメント

コメント

まだコメントはありません。

ログインするとコメントできます