Redis(および互換実装の Valkey など)を本番で安全・高速・低コストに運用するための実践ガイドです。
目次
- キー設計
- データ型の選び方
- メモリ管理
- TTL と有効期限
- パフォーマンス
- 避けるべきコマンド・アンチパターン
- キャッシュ戦略
- 永続化
- 高可用性とスケーリング
- セキュリティ
- クライアント・接続管理
- 監視・運用
- チェックリスト
1. キー設計
命名規則を統一する
コロン区切りの階層構造が一般的です。
<アプリ/サービス>:<エンティティ>:<ID>:<属性>
例) shop:user:1001:profile
shop:cart:1001
shop:session:ab12cd34
- 一貫性: チーム全体で命名ルールを文書化する
- 適度な短さ: キー名もメモリを消費する。数億キーの規模では
user: → u: のような短縮も検討(可読性とのトレードオフ)
- バージョン付与: スキーマ変更に備えて
v2:user:1001 のようにプレフィックスでバージョン管理すると移行が楽
ホットキーを避ける
1つのキーにアクセスが集中すると、Cluster 環境では特定ノードがボトルネックになります。
- カウンタは
counter:{id}:shard:N のように分割し、読み取り時に合算する
- 読み取り集中ならアプリ側のローカルキャッシュ(クライアントサイドキャッシュ)を併用する
ビッグキーを避ける
- 目安: String は 10KB 以下、コレクション型は 要素数 数千〜1万以下 に抑える
- 巨大な Hash / List / Set / ZSet は削除・取得時にメインスレッドをブロックする
- 大きなデータは分割する(例:
user:1001:followers:0, ...:1 …)
Cluster のハッシュタグ
複数キー操作(MGET、トランザクション、Lua)を Cluster で行う場合、{} で同一スロットに寄せます。
{user:1001}:profile
{user:1001}:cart
ただし乱用するとスロットが偏るので、必要な範囲に限定します。
2. データ型の選び方
| 用途 | 推奨データ型 | 備考 |
|---|
| 単純なキャッシュ・カウンタ | String | INCR / INCRBY はアトミック |
| オブジェクト(複数フィールド) | Hash | 小さい Hash はメモリ効率の良いエンコーディングになる |
| キュー(簡易) | List | LPUSH + BRPOP |
| 信頼性のあるキュー / イベントログ | Stream | コンシューマグループ・ACK・再配信が可能 |
| ユニークな集合 | Set | 和・積・差集合 |
| ランキング・優先度付きキュー・時系列インデックス | Sorted Set | スコア順の範囲取得が O(log N + M) |
| ユニーク数の概算 | HyperLogLog | 約 12KB で誤差 ~0.81% |
| フラグ・日次アクティブ判定 | Bitmap | 1ユーザー 1bit |
| 位置情報 | Geo | 内部は Sorted Set |
| 構造化ドキュメント | JSON(モジュール/新しい版では標準搭載) | 部分更新が可能 |
ポイント
- JSON 文字列を String に丸ごと入れるより Hash の方が有利な場合が多い(部分取得・部分更新が可能)
- 小さな Hash を多数作ると、コンパクトなエンコーディング(listpack)が効いてメモリ効率が上がる
- Pub/Sub は「撃ちっぱなし」で、購読者がいない間のメッセージは失われる。取りこぼしが許されないなら Stream を使う
3. メモリ管理
maxmemory を必ず設定する
maxmemory 4gb
maxmemory-policy allkeys-lru
- 物理メモリの全量を割り当てない。fork(RDB/AOF書き換え)時の Copy-on-Write やバッファ分の余裕として、物理メモリの 60〜75% 程度を目安にする
退避ポリシー(maxmemory-policy)の選択
| ポリシー | 適した用途 |
|---|
allkeys-lru / allkeys-lfu | 純粋なキャッシュ用途(最も一般的) |
volatile-lru / volatile-lfu | キャッシュと永続データが混在(TTL付きキーのみ削除) |
volatile-ttl | 期限が近いものから消したい場合 |
noeviction | データを失ってはいけない用途(書き込みがエラーになる) |
- アクセス頻度に偏りがあるなら LFU の方がヒット率が高くなりやすい
- キャッシュと永続データ(セッション・キュー等)は別インスタンスに分けるのが理想
メモリの確認
INFO memory # used_memory, mem_fragmentation_ratio など
MEMORY USAGE <key> # キー単位の使用量
MEMORY DOCTOR # 診断
redis-cli --bigkeys # 大きなキーの検出
redis-cli --memkeys # メモリ使用量の大きいキー
mem_fragmentation_ratio が 1.5 を大きく超える場合は activedefrag yes を検討
- 1.0 を下回る場合はスワップが発生している可能性がある(要対処)
4. TTL と有効期限
- キャッシュには必ず TTL を設定する。TTL なしのキャッシュキーはメモリリークの原因になる
- 書き込みと同時に設定する:
SET key value EX 3600(SET してから EXPIRE だとアトミックでない)
- TTL にジッター(ランダム幅)を加える: 大量のキーが同時に失効すると、バックエンドへのアクセスが一斉に集中する(キャッシュスタンピード)
ttl = 3600 + random.randint(0, 300)
r.set(key, value, ex=ttl)
- 上書き時の注意:
SET は既存の TTL を消す。TTL を保持したい場合は SET ... KEEPTTL
- Hash のフィールド単位で期限を設定する機能(
HEXPIRE 等)は新しいバージョンで利用可能。使用中のバージョンを確認すること
5. パフォーマンス
ラウンドトリップを減らす
- パイプライン: 複数コマンドをまとめて送信する(往復回数が劇的に減る)
- 複数キーコマンド:
MGET / MSET / HMGET
- 1回のパイプラインは数百〜数千コマンド程度に区切る(巨大すぎるとメモリとレイテンシが悪化)
pipe = r.pipeline(transaction=False)
for uid in user_ids:
pipe.hgetall(f"user:{uid}")
results = pipe.execute()
アトミックな処理
- 複数ステップをアトミックにしたい場合は Lua スクリプト(
EVAL / EVALSHA)または Functions を使う
MULTI/EXEC はコマンドをまとめて実行するがロールバックはない。楽観ロックが必要なら WATCH と組み合わせる
- Lua スクリプトは実行中にサーバーをブロックするので、短く保つこと
計算量を意識する
- O(N) のコマンドは N が小さいことが分かっている場合のみ使う
- Sorted Set の範囲取得は件数を
LIMIT で絞る
削除は非同期で
- 大きなキーの削除は
DEL ではなく UNLINK(バックグラウンドで解放)
- 以下を有効化して、内部的な削除処理も非同期化する
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
lazyfree-lazy-user-del yes
6. 避けるべきコマンド・アンチパターン
| NG | 理由 | 代替 |
|---|
KEYS * | 全キー走査でサーバーをブロック | SCAN + MATCH + COUNT |
巨大コレクションへの HGETALL / SMEMBERS / LRANGE 0 -1 | 大量データ転送とブロック | HSCAN / SSCAN / ページング |
本番での FLUSHALL / FLUSHDB | 全データ消失 + ブロック | ACL で禁止、必要時は ASYNC |
本番での MONITOR 常用 | スループットが大幅に低下 | 短時間のデバッグのみ |
DEL で巨大キー削除 | ブロック | UNLINK |
| Redis を唯一のデータストアとして扱う(無対策) | 障害時のデータ損失 | 永続化設定 + 正のデータは RDB 等に |
| リクエストごとに新規接続 | 接続コストが大きい | コネクションプール |
| 長時間実行の Lua スクリプト | 全体をブロック | 処理を分割 |
危険コマンドの無効化(ACL 推奨)
ACL SETUSER app on >password ~app:* +@all -@dangerous
古い手法の rename-command より ACL で権限を絞る方が管理しやすいです。
7. キャッシュ戦略
主なパターン
| パターン | 概要 | 向いているケース |
|---|
| Cache-Aside(Lazy Loading) | アプリがキャッシュを確認し、なければ DB から読んで格納 | 最も一般的・汎用的 |
| Write-Through | 書き込み時に DB とキャッシュを同時更新 | 読み取り一貫性を重視 |
| Write-Behind | キャッシュに書いて非同期で DB 反映 | 書き込み性能重視(データ損失リスクあり) |
Cache-Aside の例
def get_user(uid):
key = f"user:{uid}"
cached = r.get(key)
if cached is not None:
return deserialize(cached)
user = db.fetch_user(uid)
r.set(key, serialize(user), ex=3600 + random.randint(0, 300))
return user
典型的な問題と対策
| 問題 | 内容 | 対策 |
|---|
| キャッシュスタンピード | 人気キーの失効直後に大量リクエストが DB に殺到 | ロック(SET lock NX EX)で1リクエストだけ再生成 / 期限前の先行更新 / TTL ジッター |
| キャッシュ貫通(Penetration) | 存在しないキーへの問い合わせが毎回 DB に届く | 「存在しない」結果も短い TTL でキャッシュ / Bloom Filter |
| キャッシュ雪崩(Avalanche) | 大量キーの同時失効や Redis 停止 | TTL ジッター / 高可用構成 / サーキットブレーカー |
| 不整合 | DB 更新後もキャッシュが古い | 更新時は「DB 更新 → キャッシュ削除」を基本にする |
分散ロックの注意
SET lock:resource <一意な値> NX PX 30000 で取得
- 解放時は「値が自分のものか確認してから削除」を Lua でアトミックに行う
- 正確性が厳密に求められるロック(金銭処理など)は、フェンシングトークンを使うか、より強い一貫性を持つ仕組み(DB のロック等)を検討する
8. 永続化
| 方式 | 特徴 | データ損失の可能性 |
|---|
| なし | 純キャッシュ | 再起動で全消失 |
| RDB | 定期スナップショット。復旧が速くファイルが小さい | 最後のスナップショット以降 |
| AOF | 書き込みログ。appendfsync everysec が一般的 | 最大約1秒 |
| RDB + AOF | 両方の利点。AOF の書き換えにも RDB 形式が使われる | 最大約1秒 |
推奨
- 純キャッシュ → 永続化なし、または RDB のみ
- 失いたくないデータ → AOF(
everysec)+ RDB
- バックアップとして RDB ファイルを別の場所(オブジェクトストレージ等)に定期コピーする
- 定期的にリストア手順をテストする
- fork 時のレイテンシに注意。Linux では以下を推奨:
vm.overcommit_memory = 1
- Transparent Huge Pages(THP)を無効化
9. 高可用性とスケーリング
構成の選択肢
| 構成 | 用途 |
|---|
| 単一インスタンス | 開発・検証、失っても問題ないキャッシュ |
| レプリケーション + Sentinel | データ量が1ノードに収まり、自動フェイルオーバーが必要 |
| Redis Cluster | データ量・スループットを水平分割したい |
| マネージドサービス(ElastiCache / MemoryStore / Azure Cache 等) | 運用負荷を下げたい |
ポイント
- レプリケーションは非同期。フェイルオーバー時に直近の書き込みが失われる可能性がある
WAIT コマンドで同期を待てるが、強い一貫性は保証されない
min-replicas-to-write / min-replicas-max-lag で、レプリカと切り離されたマスターへの書き込みを抑制できる
- 読み取りをレプリカに分散する場合、レプリケーション遅延(古いデータ)を許容できるかを確認する
- Cluster ではマルチキー操作に制約がある(ハッシュタグで回避)
- Sentinel は 3台以上(奇数) を別々の障害ドメインに配置する
- Cluster 対応クライアントを使い、
MOVED / ASK リダイレクトを正しく処理させる
10. セキュリティ
- インターネットに直接公開しない。プライベートネットワーク内に置き、
bind で待ち受けを限定
protected-mode yes を維持する
- ACL で認証と権限を設定(デフォルトユーザーの無効化・用途別ユーザー・キーパターン制限)
ACL SETUSER default off
ACL SETUSER app on >strong-password ~app:* &app:* +@read +@write -@dangerous
ACL SETUSER monitor on >another-password +info +ping +client|list
- TLS を有効化(特にネットワークを跨ぐ場合)
- パスワードはコードに直書きせず、シークレット管理(Vault / Secrets Manager 等)を使う
- 不要・危険なコマンド(
FLUSHALL, CONFIG, DEBUG, KEYS 等)をアプリ用ユーザーから剥奪
- 定期的にバージョンアップし、セキュリティ修正を適用する
- Lua やモジュールに関する脆弱性情報もウォッチする
11. クライアント・接続管理
- コネクションプールを使う(リクエストごとの接続確立は避ける)
- タイムアウトを必ず設定する
- 接続タイムアウト・コマンドタイムアウトを短めに(例: 数百ms〜数秒)
- リトライは指数バックオフ + ジッターで。即時の無限リトライは障害を悪化させる
- Redis 障害時にアプリ全体が止まらないよう、キャッシュ障害時は DB にフォールバックする設計にする(サーキットブレーカー)
maxclients と OS のファイルディスクリプタ上限(ulimit -n)を確認する
timeout(アイドル接続の切断)や tcp-keepalive を適切に設定する
CLIENT SETNAME で接続に名前を付けると、CLIENT LIST での調査が楽になる
- 読み取りが非常に多いなら、クライアントサイドキャッシュ(
CLIENT TRACKING)も検討
12. 監視・運用
監視すべき主要メトリクス
| メトリクス | 確認方法 | 注目点 |
|---|
| メモリ使用量 | INFO memory | used_memory vs maxmemory、断片化率 |
| ヒット率 | INFO stats | keyspace_hits / (hits + misses) |
| 退避数 | INFO stats | evicted_keys の急増はメモリ不足のサイン |
| 接続数 | INFO clients | connected_clients、blocked_clients |
| レイテンシ | LATENCY DOCTOR / redis-cli --latency | スパイクの有無 |
| スローコマンド | SLOWLOG GET | 想定外の重いコマンド |
| レプリケーション | INFO replication | 遅延(オフセット差)、リンク状態 |
| 永続化 | INFO persistence | rdb_last_bgsave_status、AOF 書き換えの状態 |
| CPU | OS / INFO cpu | Redis のコマンド処理は基本シングルスレッド |
設定例
slowlog-log-slower-than 10000 # 10ms 以上を記録(単位: マイクロ秒)
slowlog-max-len 1024
latency-monitor-threshold 100 # 100ms 以上を記録
運用のコツ
- Prometheus(redis_exporter)+ Grafana などでダッシュボード化し、アラートを設定する
- メモリ使用率・退避数・レプリケーション遅延・接続数にはアラートを必ず設ける
- 負荷試験は
redis-benchmark や memtier_benchmark で、本番に近いデータサイズ・コマンド比率で行う
- I/O スレッド(
io-threads)はネットワーク処理がボトルネックの高負荷環境で効果がある。導入前にベンチマークで確認する
13. チェックリスト
設計
設定
セキュリティ
アプリケーション
運用
参考