はじめに
Redisは高速なインメモリデータストアとして広く使われていますが、適切に使わないとパフォーマンス問題やデータ不整合を引き起こすことがあります。本記事では、Redisを効率的に使うための4つのポイントを、具体例を交えながら解説していきます。
対象者
この記事は下記のような人を対象にしています。
- Redisを使い始めたばかりで、基本的な使い方を学びたい方
- Redisのパフォーマンスを改善したいと考えている方
- Redisのトランザクションやデータ構造について理解を深めたい方
- 本番環境でRedisを安全に運用したい方
1. キー走査の問題と対策
KEYS vs SCAN:ブロッキングの違い
Redisでキーを検索する際、KEYSコマンドとSCANコマンドの2つの選択肢があります。しかし、この2つは動作が大きく異なりますね。
KEYSコマンドの問題点
# 本番環境では絶対に使ってはいけません
KEYS user:*
KEYSはすべてのキーを一度に走査し、パターンにマッチするものを返します。この処理中、Redisはブロックされ、他のクライアントからのリクエストを処理できません。数万キーが存在する環境でKEYSを実行すると、数秒間Redisが応答不能になる可能性があります。
SCANコマンドによる解決
# カーソルベースの走査(0から開始)
SCAN 0 MATCH user:* COUNT 100
# 返り値: ["6", ["user:1001", "user:1002", ...]]
# 次のカーソルが6なので続けて実行
SCAN 6 MATCH user:* COUNT 100
SCANはカーソルベースで動作し、少しずつキーを返します。各呼び出しは短時間で完了するため、他のクライアントへの影響を最小限に抑えられます。カーソルが0に戻ったら走査完了となりますね。
SCANの落とし穴:全キー走査は避けられない
SCANを使えば安全、と思いがちですが、実は重要な落とし穴があります。
SCAN 0 MATCH session:active:* COUNT 100
このMATCHパターンは、走査範囲を絞るわけではありません。SCANは内部的にすべてのキーを走査し、パターンに一致するものだけをフィルタして返します。つまり、100万キーの中から10個のsession:active:*キーを探す場合でも、100万キー全体を走査することになります。
対策:適切なデータ構造で管理する
全キー走査を避けるには、キーの管理方法を見直す必要があります。
改善前(非効率)
# 個別のキーとして保存
SET session:active:abc123 "user_data_1"
SET session:active:def456 "user_data_2"
SET session:active:ghi789 "user_data_3"
# 取得には全キー走査が必要
SCAN 0 MATCH session:active:*
改善後(効率的)
# Setで管理
SADD session:active abc123 def456 ghi789
# O(1)で存在確認
SISMEMBER session:active abc123
# O(N)で全メンバー取得(Nはセッション数のみ)
SMEMBERS session:active
Setを使えば、キー走査なしで目的のデータにアクセスできます。これは計算量がO(N)(全キー数)からO(M)(Set内の要素数)に改善されることを意味しますね。
2. データ構造の使い分け
Redisは単なるキーバリューストアではなく、複数のデータ構造を提供しています。適切な構造を選ぶことで、コードの簡潔性とパフォーマンスが大きく向上します。
String:単純なキーバリュー
最もシンプルな構造です。1つのキーに1つの値を保存します。
# ユーザーのログイン状態を保存
SET user:1001:logged_in "true"
GET user:1001:logged_in
# カウンターとして使用
INCR page:views:home
# => 1
INCR page:views:home
# => 2
計算量:GET/SETはO(1)です。単純な値の保存に適しています。
Hash:オブジェクト的なデータ
複数のフィールドを持つオブジェクトを表現します。
# ユーザー情報を保存
HSET user:1001 name "太郎" email "taro@example.com" age "25"
# 全フィールドを取得
HGETALL user:1001
# => ["name", "太郎", "email", "taro@example.com", "age", "25"]
# 特定フィールドのみ取得
HGET user:1001 name
# => "太郎"
計算量:HGET/HSETはO(1)、HGETALLはO(N)(Nはフィールド数)です。
Hashを使うメリットは、複数のStringキーを1つのHashにまとめることで、キーの総数を削減できる点にあります。例えば、ユーザー1000人の情報をuser:1001:name、user:1001:emailのように保存すると2000キーになりますが、Hashなら1000キーで済みますね。
Set:重複なし集合
順序なしの一意な要素の集合です。
# タグの管理
SADD article:101:tags "Redis" "NoSQL" "Database"
SADD article:102:tags "Redis" "Performance"
# 共通タグの取得(積集合)
SINTER article:101:tags article:102:tags
# => ["Redis"]
# 全タグの取得(和集合)
SUNION article:101:tags article:102:tags
# => ["Redis", "NoSQL", "Database", "Performance"]
# 存在確認
SISMEMBER article:101:tags "Redis"
# => 1 (true)
計算量:SADD/SISMEMBERはO(1)、SMEMBERSはO(N)(N=要素数)です。
Sorted Set:スコア付き集合
各要素にスコア(数値)が付いた集合です。スコア順にソートされており、範囲取得や削除が効率的に行えます。
# ランキングの管理(スコアは得点)
ZADD game:ranking 1500 "player_A" 2000 "player_B" 1800 "player_C"
# スコア順で取得(上位3人)
ZREVRANGE game:ranking 0 2 WITHSCORES
# => ["player_B", "2000", "player_C", "1800", "player_A", "1500"]
# スコア範囲で削除(1000点未満を削除)
ZREMRANGEBYSCORE game:ranking -inf 1000
# 特定のスコア範囲を取得
ZRANGEBYSCORE game:ranking 1500 2000
# => ["player_A", "player_C", "player_B"]
計算量:ZADDはO(log N)、ZRANGEはO(log N + M)(M=取得要素数)です。
Sorted Setは、ランキング、タイムライン、優先度付きキューなど、順序が重要な場面で威力を発揮しますね。
3. トランザクションと原子性
Redisのトランザクションは、一般的なRDBMSとは異なる特性を持っています。この違いを理解しないと、データ不整合を引き起こす可能性がありますので注意が必要です。
MULTI/EXEC:パイプライン + 割り込み防止
MULTI
INCR counter
INCR counter
INCR counter
EXEC
# => [1, 2, 3]
MULTI〜EXECは、複数のコマンドをまとめて実行します。この間、他のクライアントからのコマンドは割り込みません。しかし、重要な点が2つあります。
1. ロールバックはありません
MULTI
SET key1 "value1"
INCR key1 # エラー:文字列をインクリメントできません
SET key2 "value2"
EXEC
# => [OK, (error), OK]
途中でエラーが発生しても、他のコマンドは実行されます。key1とkey2は両方とも設定されます。つまり、失敗したら全部巻き戻す、という動作はしません。
2. 条件分岐はできません
MULTI
GET counter
# => counterの値を見て次の処理を変えたい?できません
SET another_key "value"
EXEC
MULTI内のコマンドは、実行前にすべてキューイングされます。GETの結果を見て次の処理を変える、といった条件分岐はできません。
WATCH:楽観的ロック
# counter の現在値を監視
WATCH counter
current_value=\$(GET counter)
# 他のクライアントがcounterを変更したらトランザクション失敗
MULTI
SET counter \$((current_value + 1))
EXEC
# => counterが変更されていなければ成功、されていればnil
WATCHは指定したキーを監視し、EXECまでに他のクライアントがそのキーを変更したら、トランザクション全体を中断します。これは楽観的ロックと呼ばれる仕組みで、競合が少ない場合に効率的です。
ただし、競合が頻繁に発生する場合は、リトライロジックが必要になり、コードが複雑になりますね。
Luaスクリプト:真の原子性
-- Luaスクリプト(increment_with_limit.lua)
local current = redis.call('GET', KEYS[1])
if current == false then
current = 0
else
current = tonumber(current)
end
if current >= tonumber(ARGV[1]) then
return {err = "limit exceeded"}
end
return redis.call('INCR', KEYS[1])
# スクリプトを実行(上限10)
EVAL "\$(cat increment_with_limit.lua)" 1 counter 10
Luaスクリプトは、Redis内部で原子的に実行されます。条件分岐、ループ、複雑なロジックをすべて1つのアトミックな操作として実行できます。
Luaの利点:
- 完全なアトミック性(スクリプト全体が不可分)
- ネットワークラウンドトリップの削減(複数コマンドを1回で実行)
- 条件分岐やループが可能
欠点:
- スクリプトのデバッグが難しい
- 長時間実行すると他のクライアントをブロックしてしまう
DBトランザクション(ACID)との違い
RedisのトランザクションとRDBMSのACIDトランザクションは、名前は同じでも別物です。
| 特性 | RDBMS (ACID) | Redis (MULTI/EXEC) | Redis (Lua) |
|---|---|---|---|
| Atomicity | ○ ロールバック可能 | △ ロールバック不可 | ○ 完全 |
| Consistency | ○ 制約チェック | × アプリ側で保証 | △ スクリプト内で実装 |
| Isolation | ○ 分離レベル選択可 | ○ 完全分離 | ○ 完全分離 |
| Durability | ○ コミット時永続化 | △ 設定次第 | △ 設定次第 |
Redisは速度を優先するため、厳密なACIDは保証しません。データ整合性が重要な場合は、Luaスクリプトか、RDBMSの併用を検討することをおすすめします。
4. TTLと有効期限管理
Redisはキャッシュとして使われることが多く、データに有効期限を設定する機能が充実しています。
基本的なTTL設定
# 値を設定して60秒後に自動削除
SETEX session:abc123 60 "user_data"
# 既存のキーに30秒のTTLを設定
EXPIRE session:abc123 30
# TTL確認
TTL session:abc123
# => 28 (残り28秒)
# TTLを削除(永続化)
PERSIST session:abc123
計算量:すべてO(1)です。
GETEX:読み取り時にTTL更新
Redis 6.2以降では、GETEXコマンドで値を取得しながらTTLを更新できます。
# セッションデータを取得し、TTLを60秒に更新
GETEX session:abc123 EX 60
これは、アクセスされたセッションの有効期限を延長するLRU(Least Recently Used)キャッシュのような動作を実現できますね。
従来はGET + EXPIREの2コマンドが必要でしたが、GETEXなら1コマンドで済み、アトミック性も保証されます。
Sorted Setのスコアでの期限管理パターン
TTLは便利ですが、「期限切れのキーを一括で削除したい」場合には不向きです。なぜなら、TTL付きキーを列挙する効率的な方法がないからです(SCANで全キー走査が必要になります)。
この問題は、Sorted Setで解決できます。
# Unixタイムスタンプをスコアとして保存
ZADD session:expiry 1706745600 "session:abc123"
ZADD session:expiry 1706749200 "session:def456"
# 現在時刻より古いセッションを削除
# 例:2026-01-21 12:00:00 (1706756400) 時点で実行
ZREMRANGEBYSCORE session:expiry -inf 1706756400
# 削除対象を先に確認することも可能
ZRANGEBYSCORE session:expiry -inf 1706756400
# => ["session:abc123", "session:def456"]
この方法のメリット:
- 期限切れのキーを
O(log N + M)で効率的に取得・削除可能 - 削除前に確認できる(ログ記録やクリーンアップ処理)
- 一括削除が容易
注意点:
- Sorted Set自体のメンテナンスが必要
- 実際のデータ(
session:abc123の内容)は別途削除が必要
実装例:
# 1. セッションを作成
SET session:abc123 "user_data" EX 3600
ZADD session:expiry 1706749200 "session:abc123"
# 2. 定期的にクリーンアップ(cronなど)
ZRANGEBYSCORE session:expiry -inf \$(date +%s)
# => 期限切れセッションのリスト
# 各セッションを削除
ZREMRANGEBYSCORE session:expiry -inf \$(date +%s)
複雑に見えますが、大量のキーを効率的に管理する場合、この方法が最も実用的です。
おわりに
本記事では、Redisを効率的に使うためのポイントについてまとめました。
キー走査
KEYSは本番環境で使わないでくださいSCANでもパターンマッチは全キー走査になります- Set/Sorted Setでキーを管理し、走査を回避しましょう
データ構造
- String:単純な値、カウンター
- Hash:オブジェクト、キー数削減
- Set:一意な集合、集合演算
- Sorted Set:順序が重要なデータ、範囲操作
トランザクション
MULTI/EXEC:ロールバックなし、条件分岐不可WATCH:楽観的ロック、競合時はリトライ- Luaスクリプト:完全な原子性、複雑なロジック
- RDBMSのACIDとは別物です
TTL管理
SETEX/EXPIRE:基本的なTTL設定GETEX:読み取り時にTTL更新- Sorted Set:期限切れキーの一括管理
これらを理解すれば、Redisのパフォーマンスを最大限に引き出し、安全に運用できます。データ構造の選択とトランザクションの理解が、特に重要ですね。
この記事がどなたかの参考になれば幸いです。