✍️ 필사 모드: Redis 内部と分散キャッシュ — シングルスレッド、データ構造、Cluster、Sentinel、RDB/AOF、Redlock、Valkey、Dragonfly 完全解説 (2025)
日本語"Redis is what happens when a C programmer falls in love with data structures." — Salvatore Sanfilippo (antirez)
Redis ほど「とりあえず使う」と「きちんと理解して使う」の差が大きいシステムは珍しい。ほとんどの開発者は GET/SET しか使わず、しかも文字列キャッシュ用途だけ。しかし Redis は本来、インメモリデータ構造サーバー (In-Memory Data Structure Server) であり、キャッシュは副産物に過ぎない。
2009 年、Salvatore Sanfilippo が自身のウェブ解析ツール LLOOGG のために作った。MySQL ではリアルタイムランキングを維持できず、自作した「Remote Dictionary Server」が Redis の始まりである。2010 年に VMware、2013 年に Pivotal、2015 年に Redis Labs (現 Redis Inc) へと移り、そして 2024 年 3 月、Redis は突如オープンソースライセンスを捨てた。Linux Foundation が Valkey をフォークし、クラウド戦争の新局面が開いた。
本稿は Redis を「本当に」理解したい人のための地図である。
1. なぜ Redis は速いのか — シングルスレッドの逆説
よくある誤解
「シングルスレッドなのに速い? コアを遊ばせているのでは?」
違う。Redis 6.0 以降、ネットワーク I/O はマルチスレッドで処理するが、コマンド実行そのものは依然シングルスレッド。そしてそれこそが速さの理由である。
シングルスレッドの合理性
- メモリ速度は CPU 速度に近い — ボトルネックはネットワークと OS 呼び出し
- ロックなし — 共有構造のミューテックスオーバーヘッドゼロ
- コンテキストスイッチなし — CPU キャッシュ局所性を最大化
- 原子性が自然 — すべてのコマンドが atomic
- デバッグが容易 — バグ再現が単純
antirez の言葉:「2009 年当時すでに、ロックベースのマルチスレッドが現実には難しいと分かっていた。ロックなしで速いものを作ろう」
100 万 QPS の秘密
実際に Redis は単一インスタンスで 毎秒 100 万コマンド を超える。秘訣は:
- I/O 多重化 —
epoll(Linux) /kqueue(BSD) イベントループで 1 スレッドから数千ソケットを監視 - RESP プロトコル — 単純なテキストベース、パース費用最小
- パイプライニング — 複数コマンドをまとめて送受信
- ゼロコピーではない — しかし単位が小さく問題なし
- 大半が または の構造
イベントループ 1 周
1. epoll_wait() — 準備済みソケット検出
2. リクエストパース (RESP)
3. コマンド実行 (シングルスレッド、メモリ操作のみ)
4. レスポンスバッファ書き込み
5. 次のイベントへ
1 コマンドが長いと? 全体が止まる。だから KEYS * や FLUSHALL は本番禁止。代わりに SCAN、UNLINK を使う。
2. データ構造 — Redis の真の力
OBJECT ENCODING mykey で内部エンコーディングを見ると驚く。同じ「String」でも数値なら int、短ければ embstr、長ければ raw。
9 つの中核データ構造
| 構造 | 用途 | 内部実装 | 特徴 |
|---|---|---|---|
| String | 文字列/数値/バイナリ | SDS | 最大 512MB |
| List | キュー/スタック | QuickList | 双方向 O(1) push/pop |
| Hash | オブジェクトフィールド | listpack/hashtable | 小ハッシュは ziplist |
| Set | ユニーク値集合 | listpack/intset/hashtable | 整数のみなら intset |
| Sorted Set | ランキング | Skip List + hash | 歴史的に興味深い選択 |
| Stream | イベントログ | Radix Tree | Kafka 風 consumer group |
| HyperLogLog | カーディナリティ推定 | 固定 12KB、標準偏差 0.81% | 確率的構造 |
| Bitmap | ビット配列 | String ベース | 10 億ユーザー DAU を 128MB に |
| Geospatial | 位置クエリ | Sorted Set + Geohash | GEOADD/GEORADIUS |
SDS — なぜ C 文字列ではないのか
struct sdshdr {
int len; // O(1) strlen
int free; // 余剰領域
char buf[]; // 実データ + '\0'
};
strlenO(1) (C 文字列は O(N))- バイナリセーフ (途中に
\0可) - 再割り当て削減 (2 倍バッファ)
Sorted Set の Skip List — なぜ Red-Black Tree ではないのか
antirez が自身のブログで理由を述べている:
- 実装が単純 — B-Tree/Red-Black の半分以下
- Range query に強い — ソート済み連結リスト構造
- メモリ局所性がそこそこ良い
- デバッグ容易 — 木の回転なし
「Skip List を選んだのは最適だったからではなく、実装が単純だったからだ」— antirez
HyperLogLog — 12KB で数十億推定
- 「今日の UV は?」→ Set ではメモリ爆発
- HLL は確率的で標準偏差 0.81%
- 固定 12KB — 1 億でも 100 億でも 12KB
PFADD visitors user:1 user:2 user:3
PFCOUNT visitors
PFMERGE today yesterday
Bitmap — SETBIT の威力
SETBIT user:active:20260415 12345 1
BITCOUNT user:active:20260415
BITOP AND weekly user:active:*
10 億ユーザー → 10 億ビット → 128MB。「過去 30 日連続ログインユーザー」のクエリが高速。
Stream — Kafka-lite (2018)
Redis 5.0 で追加。Consumer Group と offset 管理で Kafka-lite 用途。ただし永続性は Redis の persistence 依存なので、Kafka 代替というより軽量メッセージバス。
3. 永続化 — RDB vs AOF のトレードオフ
RDB (Redis Database) — スナップショット
- 定期的に全データセットをバイナリファイルへダンプ
BGSAVE— fork() 後に子プロセスがダンプ、親は応答継続- Copy-on-Write により親メモリは通常 2 倍にならない
- 欠点:fork 以降のデータは失われる可能性
- 利点:再起動が速い、ファイルサイズ小
AOF (Append-Only File) — ログ
- 全書き込みコマンドをファイル末尾に追記
fsyncポリシー:always— 毎コマンド fsync (遅い、損失なし)everysec— 毎秒 (デフォルト、最大 1 秒損失)no— OS 任せ (速い、損失大)
- AOF Rewrite — 定期圧縮
- 欠点:再起動が遅い、ファイル大
- 利点:損失ほぼなし
ハイブリッド — Redis 4.0+
aof-use-rdb-preamble yes — AOF 先頭に RDB、末尾に増分 AOF。高速再起動と低損失の両立。現在の事実上の標準。
意思決定マトリクス
| シナリオ | RDB | AOF | ハイブリッド |
|---|---|---|---|
| キャッシュ | OK (永続化不要) | X | X |
| セッション保存 | X | OK (everysec) | OK |
| 再起動時間重視 | OK | X | OK |
| 主ストレージ | X | OK (always) | OK (最良) |
| ディスク I/O 敏感 | OK | X | 注意 |
本当に永続ストアとして使えるか
推奨しない。antirez も繰り返し警告した。Redis の目標は「高速キャッシュと最小損失」であり、「完全な耐久性」ではない。重要データは Postgres/MySQL へ、Redis はキャッシュに。
4. Redis Cluster — Hash Slot の芸術
なぜ Cluster か
- 単一 Redis はメモリ/スループット上限 (通常数十 GB)
- マルチシャード必要 → Redis Cluster (3.0、2015)
16384 Hash Slot
- キーを CRC16 でハッシュ後、16384 (2^14) でモジュロ
- 各スロットをマスターノードへ割り当て
- 3 マスターなら各 ~5461 スロット
なぜ 16384 か
antirez が GitHub issue で直接答えた:
- スロットビットマップを gossip で送信 — 16384 ビット = 2KB
- 65536 だと 8KB — gossip では大きすぎる
- 1000 ノード未満なら 16384 で分配品質十分
MOVED & ASK — リダイレクト
MOVED <slot> host:port— 「このスロットは恒久的にそこ」ASK <slot> host:port— 「移行中、一度だけそこに聞け」
スマートクライアント (Lettuce、redis-py-cluster) はスロットマップをキャッシュし、MOVED で更新する。
Hash Tag — 同一スロット保証
SET {user:1}:profile "..."
SET {user:1}:sessions "..."
{} 内の部分のみハッシュ対象。MULTI/EXEC、SUNION、複数キーを扱う Lua で必須。
Gossip と障害検出
- ノード間で PING/PONG
cluster-node-timeout超 →PFAIL(主観障害)- 過半数が PFAIL 報告 →
FAIL(客観障害) - レプリカが自動昇格
制約
- マルチキーコマンド (SINTER) は同一スロット必須
- クロススロットトランザクション不可
- バックアップが複雑 (ノード別)
5. Sentinel — HA のもう一つの道
Cluster がシャーディングと HA を兼ねる一方、Sentinel は HA のみ を提供する。
構成
- マスター 1 + レプリカ N + Sentinel M (通常 3 以上、奇数)
- Sentinel 群がマスター状態を監視
- 障害時は Raft 風合意で新リーダー選出
- クライアントはまず Sentinel に現在のマスターを問い合わせ
Cluster vs Sentinel
| 側面 | Sentinel | Cluster |
|---|---|---|
| シャーディング | X | OK |
| HA | OK | OK |
| 設定複雑度 | 低 | 中 |
| クライアント対応 | 広い | スマートクライアント必要 |
| 運用規模 | 小〜中 | 中〜大 |
| マルチキー | 完全 | Hash Tag 必要 |
選択基準:データが 1 ノードメモリに収まる → Sentinel、収まらない → Cluster。
6. キャッシュパターン — ホットポテト
Cache-Aside (Lazy Loading)
def get_user(id):
user = redis.get(f"user:{id}")
if user is None:
user = db.query(id)
redis.set(f"user:{id}", user, ex=3600)
return user
- 最も一般的
- 利点:実装容易、ミス時のみ DB 負荷
- 欠点:初回リクエストが遅い、Stale データ 可能性
Write-Through
def update_user(id, data):
db.update(id, data)
redis.set(f"user:{id}", data, ex=3600)
- 書き込み毎に DB + キャッシュ更新
- 一貫性良好
- 欠点:書き込み遅延増加、読まれないデータもキャッシュ
Write-Behind (Write-Back)
def update_user(id, data):
redis.set(f"user:{id}", data)
queue.push({"id": id, "data": data})
- 書き込み遅延最小
- 欠点:データ損失リスク、実装複雑
Refresh-Ahead
- TTL 間近のキャッシュを非同期で先行更新
- Thundering Herd 防止
実戦の組合せ
多くの本番は Cache-Aside + TTL + (条件付き) Write-Through。TTL 戦略が肝:
- 短 TTL (1〜5 分) — 鮮度重視
- 長 TTL (1 時間以上) — 変更少ないデータ
- Jitter —
ex=3600 + random(-300, 300)— 同時失効回避
7. Thundering Herd とキャッシュスタンピード
Redis 障害で最も多く、最も見えにくい。
シナリオ
- 人気キーが失効
- 同時に 数千リクエストがキャッシュミス
- 全て DB 直撃 → DB 過負荷 → 全体障害
対策 1 — Mutex Lock
def get_with_lock(key):
value = redis.get(key)
if value is not None:
return value
lock = redis.set(f"lock:{key}", "1", nx=True, ex=10)
if not lock:
time.sleep(0.05)
return redis.get(key)
try:
value = db.query(...)
redis.set(key, value, ex=3600)
return value
finally:
redis.delete(f"lock:{key}")
対策 2 — 確率的先行期限切れ (XFetch)
失効前に 確率的に更新。論文 "Optimal Probabilistic Cache Stampede Prevention"。
def fetch(key, beta=1.0):
value, ttl, delta = redis.get_with_meta(key)
now = time.time()
if value is None or now - delta * beta * math.log(random.random()) >= ttl:
value = db.query(...)
redis.set(key, value, ex=3600)
return value
対策 3 — 二重 TTL (Soft & Hard)
- Soft TTL 超過でバックグラウンド更新、クライアントは旧値
- Hard TTL 超過で同期更新
8. 分散ロック — Redlock 論争
SET key value NX PX 10000 で簡単に実装できる。
単純ロック (1 インスタンス)
SET lock:resource unique_id NX PX 30000
EVAL "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 lock:resource unique_id
Redlock — 5 インスタンス分散ロック
antirez 提案のアルゴリズム:
- 5 独立インスタンスへ同時にロック試行
- 過半 (3) 成功かつ総時間 < TTL なら取得
- 失敗時は全解放
Martin Kleppmann の批判
DDIA 著者が有名なブログで反論:
- Fencing token なし — GC 停止で失効ロックで作業継続
- 時計同期仮定が弱い — NTP ジャンプ、VM 停止で TTL 保証不可
- 正確性 (correctness) が必要なら Redlock を使うな — ZooKeeper、etcd を使え
antirez の反論
- Redlock は 性能目的 — 稀な重複実行が許容できる場合のみ
- 正確性が致命的なら DB トランザクションを使え
- Fencing token は実装可能 (単調増加カウンター)
現実的結論
| 目的 | 推奨ツール |
|---|---|
| 重複防止 (性能) | Redis SET NX PX |
| リーダー選出 (正確性) | ZooKeeper/etcd |
| トランザクション | DB そのもの |
| 決済/金銭関連 | 絶対に Redis ロック禁止 |
9. 2024 年ライセンス騒動と Valkey フォーク
2024 年 3 月 20 日、Redis Inc が衝撃発表:
- Redis 7.4 から BSD → SSPL/RSALv2 デュアルライセンス
- AWS ElastiCache 等クラウド事業者への打撃が目的
- OSS コミュニティは激怒
Valkey — 48 時間での反撃
- 3 月 28 日、Linux Foundation が Valkey プロジェクト開始
- AWS、Google Cloud、Oracle、Ericsson が創設スポンサー
- Redis 7.2.4 からフォーク、BSD 3-Clause 維持
- 主要メンテナーの大半が Valkey へ移籍
antirez の復帰
2024 年 11 月、antirez が Redis Inc に復帰。2025 年はベクトル検索 (RedisVL) と AI 統合に注力。
2025 年現状
| 製品 | ライセンス | 主導 | 貢献者 |
|---|---|---|---|
| Redis | SSPL/RSALv2 | Redis Inc | antirez 復帰 |
| Valkey | BSD 3-Clause | Linux Foundation | AWS/Google/Oracle |
| KeyDB | BSD 3-Clause | Snap | マルチスレッドフォーク |
| Dragonfly | BSL/Apache 2.0 | Dragonfly Labs | ゼロから再実装 |
選択ガイド:
- パブリッククラウド managed → 気にしなくてよい (ElastiCache は Valkey へ移行中)
- 自前運用 + OSS 主義 → Valkey
- マルチスレッド極限性能 → Dragonfly
- Redis 7.4+ の新機能必要 → Redis
10. Dragonfly — "Redis を 25 倍速く"
2022 年登場、C++20 でゼロから再実装。秘訣:
- マルチスレッド Shared-nothing — 各スレッドが自シャード担当、ロック不要
- io_uring — Linux 最新の非同期 I/O (epoll より速い)
- Dash ハッシュテーブル — 学術論文ベース、キャッシュフレンドリー
- RDB 保存速度 30 倍 — メモリスナップショットアルゴリズム刷新
性能 (2025 ベンチマーク)
- 単一 AWS c7g.16xlarge:650 万 QPS (Redis は約 20 万 QPS)
- メモリ効率も 30% 改善
制約
- スクリプティング (Lua) 制限
- Cluster プロトコル 100% 互換ではない
- 一部 edge case コマンド未実装
- 運用ツール生態系が小さい
KeyDB
- Snap (Snapchat) 製マルチスレッドフォーク
- Redis とほぼ 100% 互換
- 2024 年以降開発停滞 — 勢いは Dragonfly へ
11. メモリ管理 — OOM を防ぐ 8 つの方法
Redis の OOM は即障害。
maxmemory + Eviction Policy
maxmemory 4gb
maxmemory-policy allkeys-lru
ポリシー:
noeviction— 新規書き込み拒否 (デフォルト、危険)allkeys-lru— 全キーから LRUvolatile-lru— TTL 持ちキーから LRUallkeys-lfu— LFU (4.0+、推奨)volatile-ttl— TTL 間近優先allkeys-random/volatile-random
キャッシュ用途なら allkeys-lfu を推奨。LRU はスキャン攻撃に弱い。
プロファイリング
MEMORY USAGE user:1
MEMORY STATS
MEMORY DOCTOR
Big Key 問題
単一キーが 1MB 超で危険:
- KEYS 走査コスト
- ネットワーク送信遅延
- Cluster リシャーディング時の全体移動
- 対策:ハッシュ/リストに分割
Hot Key 問題
単一キーに集中 → 1 コア全開
- 対策:クライアント側キャッシュ、シャード分散、レプリカで読み分散
TTL 戦略
- 必ず TTL 設定 — 無限キーはメモリリーク
- Jitter 追加 — 同時失効回避
- Hot は長 TTL、Cold は短 TTL
12. Lua スクリプティングと Functions
Redis は Lua 5.1 を内蔵。複数コマンドを atomic 実行。
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock >= tonumber(ARGV[1]) then
redis.call('DECRBY', KEYS[1], ARGV[1])
return 1
else
return 0
end
Functions (Redis 7.0+)
Lua スクリプトをサーバーにライブラリ保存 (関数単位管理)。
注意
- Lua 実行中は 他全コマンドブロック — 長スクリプト禁止
EVAL頻用時はSCRIPT LOAD+EVALSHA
13. Client-Side Caching (Tracking) — Redis 6.0
サーバー側変更をクライアントへ プッシュ通知。クライアントはローカルキャッシュを保持し、必要時に無効化。
CLIENT TRACKING ON REDIRECT 1234 BCAST PREFIX user:
- ラウンドトリップ削減 → 超低レイテンシ
- Lettuce、redis-py など主要ドライバが対応
- クライアント側メモリ管理必要
14. 監視指標 — 必見の 10 個
| 指標 | 説明 | 警告閾値 |
|---|---|---|
used_memory / maxmemory | メモリ使用率 | 80% |
evicted_keys | 追い出しキー数 | 増加傾向 |
connected_clients | 同時接続 | 1 万以上注意 |
instantaneous_ops_per_sec | QPS | ベースライン対比 |
latency_percentiles_usec_* | p99 遅延 | 1ms 超 |
rejected_connections | 接続拒否 | 0 維持 |
keyspace_hits / misses | ヒット率 | 90% 以上 |
aof_current_size | AOF サイズ | ディスク余裕 |
rdb_last_save_time | 最終 RDB | 古いと警告 |
master_link_status (レプリカ) | 複製接続 | up |
遅いコマンド追跡
CONFIG SET slowlog-log-slower-than 10000
SLOWLOG GET 10
MONITOR 禁止
MONITOR は全コマンドをストリーミング → 性能 50%+ 低下。本番厳禁。代わりに SLOWLOG、LATENCY。
15. キャッシュ戦略アンチパターン TOP 10
- TTL なしキー大量挿入 — メモリリーク
KEYS *やFLUSHALL— シングルスレッド停止- 巨大 Hash/List 蓄積 — 1 コマンドが秒単位
- キャッシュのみ保存 — Redis 損失で復旧不能
- 全キー同一 TTL — 同時失効 → スタンピード
- 分散ロックで金銭管理 — Redlock 論争参照
- Pub/Sub を永続キューに — メッセージ消滅、Stream を使え
- 短 TTL の Write-Through — 毎回 DB + Redis、無意味
- レプリカへ書き込み — 複製非同期、損失
- Cluster でクロススロットトランザクション試行 — 静かに失敗
16. Redis を賢く使うチェックリスト
- 用途は キャッシュか、ストレージか を明確に
-
maxmemoryと eviction policy を明示設定 - 全キーに TTL — Jitter 込み
- AOF + everysec で 1 秒以内損失許容、ハイブリッドも検討
- MONITOR / KEYS * / FLUSHALL 禁止ポリシー
- Big Key / Hot Key アラート (1MB / 10K QPS)
- ヒット率 90%+ 目標、ミスは原因分析
- Thundering Herd 対策 — Mutex か XFetch
- 分散ロックは 性能目的のみ — 正確性が必要なら別ツール
- Cluster vs Sentinel 判断基準明確 (データサイズ)
- Client-Side Caching 検討 — 読み中心ワークロード
- レプリカ昇格シナリオ訓練 (Failover 演習)
おわりに — シングルスレッドの優雅さ
Redis の成功は逆説だ。並列が王の時代に、シングルスレッドで毎秒 100 万 QPS を出す システム。その背後には antirez の哲学がある。
「複雑なものを単純にするのではなく、最初から単純に設計するのが本当のエンジニアリングだ」— antirez
多くの「レガシー」技術がそうであるように、Redis は見た目よりずっと深い。データ構造、I/O モデル、永続化、分散、トレードオフ — すべての決定に理由がある。2024 年のライセンス論争は OSS と商用化の古い緊張が Redis で噴出した事件であり、Valkey/Dragonfly の台頭は「Redis そのものよりそのインターフェースが重要になった」時代を示している。
次回予告 — PostgreSQL 内部とクエリ最適化
Redis が「データ構造の優雅さ」なら、PostgreSQL は「リレーショナル DB の完成形」。次回:
- MVCC、VACUUM、WAL と複製
- クエリプランナ内部
- B-Tree/Hash/GiST/GIN/BRIN/HNSW (pgvector)
- Partitioning、pgBouncer
- JSONB vs Document DB
- PostgreSQL 18 (2025) の新機能 (AIO、DirectIO、UUIDv7)
データベースを「ブラックボックスでなく透明なエンジン」として見る旅。
「Redis はもともと私自身のためのツールでした。1 万人が使い始めても、100 万人が使い始めても、私は『自分が使いやすい』ものを作ろうとしました。それが Redis の秘密です」— Salvatore Sanfilippo (2025 年 Redis 復帰インタビュー)
현재 단락 (1/312)
Redis ほど「とりあえず使う」と「きちんと理解して使う」の差が大きいシステムは珍しい。ほとんどの開発者は GET/SET しか使わず、しかも文字列キャッシュ用途だけ。しかし Redis は本来、*...