RedisのSCANコマンド入門|安全なキー走査方法
はじめに
Redisは高速なインメモリデータストアとして、キャッシュ、メッセージキュー、リアルタイムアプリケーションなど、幅広い用途で活用されています。その柔軟なデータ構造と優れたパフォーマンスは、多くの開発者や運用者にとって欠かせないツールとなっています。
Redisを利用する上で、蓄積されたキーを一覧したり、特定のパターンに一致するキーを探したりする必要に迫られることがあります。例えば、システムの監視、定期的なデータバックアップ、データ移行、あるいは特定の条件下で不要になったキーの一括削除など、様々な運用・保守タスクにおいて、キーの走査(Iteration)は重要な操作となります。
かつて、Redisで全てのキー、または特定のパターンに一致するキーを取得する主要な手段として、KEYS
コマンドが利用されていました。しかし、データ量の増加とともに、KEYS
コマンドには深刻な問題があることが明らかになり、本番環境での利用は非推奨とされるようになりました。KEYS
コマンドは、データ量が多い環境で実行すると、Redisサーバーを長時間ブロックし、他の全てのコマンドの処理を妨げてしまう可能性があるからです。
このような背景から、Redis 2.8で導入されたのがSCAN
コマンドとその関連コマンド(HSCAN
, SSCAN
, ZSCAN
)です。これらのコマンドは、ノンブロッキングな方法で、少量ずつサーバーからデータを取得していくイテレータベースの走査を提供し、Redisサーバーの可用性を維持しながら安全にキー空間や各種データ構造を走査することを可能にしました。
本記事では、RedisのSCAN
コマンドに焦点を当て、その基本的な使い方から、なぜKEYS
コマンドよりも安全なのか、そして実際の運用で安全かつ効果的に利用するための詳細な挙動、注意点、ベストプラクティスまでを徹底的に解説します。この記事を読むことで、あなたはRedisのキー走査を安全に行うための知識を習得し、Redisを用いたシステムの安定稼働に貢献できるようになるでしょう。
KEYSコマンドの問題点 – なぜ使うべきではないのか
SCAN
コマンドの利点を理解するためには、まずKEYS
コマンドが抱える問題を深く理解することが重要です。
KEYS pattern
コマンドは、指定されたパターンに一致する全てのキーを検索し、そのリストを一度にクライアントに返却します。例えば、KEYS *
と実行すれば、データベース内の全てのキー名が取得できます。
一見非常に便利に見えるこのコマンドですが、Redisの内部構造と処理モデルを考慮すると、大規模なデータセットに対して実行した場合に以下のような深刻な問題を引き起こす可能性があります。
-
単一スレッドモデルによるブロッキング: Redisは、そのパフォーマンスの根幹として、ほとんどの操作を単一のメインスレッドで実行します(一部のバックグラウンドタスクやモジュールはマルチスレッドを利用できますが、クライアントからのコマンド処理は基本的には単一スレッドです)。これは、ロックやコンテキストスイッチのオーバーヘッドを最小限に抑え、非常に高速な処理を実現するための設計です。しかし、このモデルの弱点は、一つのコマンドが長い時間CPUリソースを占有すると、その間、他の全てのクライアントからのコマンド処理が停止してしまうという点です。
KEYS
コマンドは、指定されたパターンに一致するキーをすべてメモリ上で探し回る処理を行います。データベースに数百万、数千万、あるいはそれ以上のキーが存在する場合、この検索処理は非常に長い時間を要します。この間、RedisのメインスレッドはKEYS
コマンドの実行に専念するため、他のどんなコマンド(GET
,SET
,LPUSH
,INCR
など、通常のアプリケーションが頻繁に利用するコマンド)も一切処理できなくなります。 -
レイテンシースパイクの発生: 前述のブロッキングにより、他のクライアントからのリクエストに対する応答時間が急激に増大します。これが「レイテンシースパイク」です。通常数十マイクロ秒から数ミリ秒で応答するはずのRedisコマンドが、
KEYS
コマンドの実行中は数十秒、場合によっては数分も応答しないといった事態が発生します。これは、Redisをデータストアとして利用しているアプリケーションの応答性を著しく低下させ、最悪の場合、アプリケーションのタイムアウトエラーを頻繁に発生させ、サービス全体の停止や不安定化を招きます。 -
メモリ使用量の増大:
KEYS
コマンドは、見つかった全てのキー名を一時的にメモリ上に格納してからクライアントに返却します。大量のキーが見つかった場合、このキー名リストを保持するためにRedisサーバーのメモリ使用量が一時的に大きく増加します。キー名自体は通常小さいデータですが、数千万個といった規模になると無視できないサイズになります。メモリが潤沢でないサーバーでは、この一時的なメモリ増加が原因で、他のデータ領域を圧迫したり、スワップが発生したり、最悪の場合はOOM(Out Of Memory) KillerによってRedisプロセスが強制終了されたりするリスクも考えられます。 -
本番環境での予測不可能性: 大規模なデータセットを持つ本番環境では、
KEYS
コマンドがいつ、どれくらいの時間サーバーをブロックするかを正確に予測することは困難です。キーの総数、キー名の長さ、サーバーのCPU性能、さらには同時実行されている他のタスクなど、様々な要因に依存するからです。予測不可能なブロッキングは、システムの可用性管理において非常に大きなリスクとなります。
これらの理由から、KEYS
コマンドは開発環境での一時的なデバッグ目的などで限定的に利用されるに留めるべきであり、本番環境で稼働中のRedisインスタンスに対して実行することは、非常に危険な行為と見なされています。Redisの公式ドキュメントでも、KEYS
コマンドの代わりにSCAN
コマンドファミリーを使用することが強く推奨されています。
SCANコマンドの基本
SCAN
コマンドは、KEYS
コマンドが抱える問題を解決するために導入された、イテレータベースのキー走査コマンドです。KEYS
コマンドが一度に全てのキーを返却するのに対し、SCAN
コマンドはサーバーに過度な負荷をかけないように、少量のキーを分割して返却します。クライアントは、サーバーから返される「カーソル」を使って、走査を段階的に進めていきます。
コマンド構文
SCAN
コマンドの基本的な構文は以下の通りです。
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
cursor
: 走査を開始するカーソルです。最初の走査を開始する際は0
を指定します。サーバーからの応答に含まれる次のカーソル値を、次のSCAN
コマンドの引数として使用します。このカーソル値が再び0
になったとき、走査は完了です。MATCH pattern
: オプションです。指定したGlobスタイルのパターンに一致するキーのみを返します。*
は任意の文字列、?
は任意の一文字、[...]
は指定された文字のいずれか、[^...]
は指定された文字以外のいずれかにマッチします。KEYS
コマンドと同様のパターンマッチングルールが適用されます。COUNT count
: オプションです。一度の走査でサーバーがチェックするエントリ数のヒントを指定します。サーバーは指定されたcount
に近い数の要素を走査しようとしますが、返される要素の数はcount
を保証するものではありません。count
のデフォルト値は10です。TYPE type
: オプションです。指定したデータ型(例:string
,list
,set
,zset
,hash
,stream
)のキーのみを返します。Redis 3.2から追加されました。
戻り値
SCAN
コマンドは、常に2つの要素を持つ配列を返します。
[string, array]
- 配列の最初の要素は、次の
SCAN
コマンドで使用すべき新しいカーソル値(文字列形式)です。走査が完了した場合は"0"
が返されます。 - 配列の2番目の要素は、現在の走査で取得されたキー名のリスト(配列)です。
例:
“`
SCAN 0
1) “17”
2) 1) “key1”
2) “key2”
3) “key3”SCAN 17
1) “0”
2) 1) “key4”
2) “key5”
“`
この例では、最初のSCAN 0
でカーソル0
から走査を開始し、次のカーソルとして17
、そしてキーkey1
, key2
, key3
を取得しました。次にSCAN 17
を実行すると、次のカーソルは0
となり、キーkey4
, key5
を取得しました。次のカーソルが0
になったため、走査は完了です。
イテレータ(カーソル)の概念
SCAN
コマンドの中核となるのが、カーソルを使ったイテレーションモデルです。
- 走査はカーソル値
0
から開始します。SCAN 0 ...
- Redisサーバーは、現在のカーソル位置から一定量の内部的なエントリを走査し、条件に一致するキーを見つけます。
- サーバーは、走査した結果として見つかったキーのリストと、次回の走査を開始すべき新しいカーソル値をクライアントに返却します。
- クライアントは、返された新しいカーソル値を使って、次の
SCAN
コマンドを実行します。 - このプロセスを、サーバーから返されるカーソル値が再び
0
になるまで繰り返します。カーソル値が0
になったら、走査は完了です。
このモデルの重要な点は、一度のSCAN
コマンドの実行は非常に短い時間で完了することです。サーバーは全キーを一度に走査するのではなく、内部的なデータ構造(後述)を少量だけ見て、次の開始位置を示すカーソルを渡すだけです。これにより、Redisのメインスレッドが長時間ブロックされることを防ぎ、他のクライアントからのコマンド処理を継続することができます。
SCANコマンドの詳細な挙動
SCAN
コマンドの内部的な挙動を理解することで、その特性(特に一貫性の欠如)をより深く理解できます。
Redisは内部的に、メインのキー空間を表現するためにハッシュテーブル(Dictionaries)を使用しています。このハッシュテーブルは配列とリンクリスト(またはSkip list、ZipListなど)の組み合わせで構成されており、要素は複数のバケット(Bucket)に分散して格納されています。ハッシュテーブルのサイズ(バケット数)は、格納されているキーの数に応じて動的にリサイズ(Rehash)されることがあります。
SCAN
コマンドは、このハッシュテーブルのバケットを順番に走査していくことでキーを探します。カーソル値は、このハッシュテーブルの走査状態、特に次に調べるべきバケットの位置などを示す情報を含んでいます。
カーソルと内部ハッシュテーブル
Redisのハッシュテーブルは、配列(バケット)の配列のような構造をしています。SCAN
のカーソルは、基本的には次に走査すべきバケットのインデックスに関連しています。
SCAN 0
で走査を開始すると、Redisは最初のバケットから順に走査を開始します。COUNT
オプションは、一度のSCAN
呼び出しでサーバーが「いくつのバケットをチェックするか」や「いくつの要素を調べるか」に対するヒントとして機能します。例えばCOUNT 100
と指定すると、Redisは効率の良い方法で約100個の要素(あるいはそれに相当するバケット数)を走査しようと試みます。
サーバーは、走査したバケットの中からMATCH
パターンやTYPE
に一致するキーを見つけ、それらをリストとして返却します。そして、次に走査を再開すべきバケットの位置などを示す新しいカーソル値を生成してクライアントに返します。クライアントはこの新しいカーソルを使って次のSCAN
を実行します。このプロセスは、全てのバケットを一度はチェックし終えるまで繰り返されます。全てのバケットをチェックし終えると、次のカーソルとして0
が返され、走査が完了します。
一貫性の欠如(Consistency)
SCAN
コマンドの最も重要な特性の一つは、その一貫性の欠如です。SCAN
コマンドは、走査を開始した時点のスナップショット上で動作するわけではありません。走査中にデータベースのキーが追加、削除、または変更された場合、その変更は走査結果に影響を与えます。
これは、SCAN
が特定の時点でのデータベースの状態を完全に把握することを目的としていないからです。代わりに、SCAN
はハッシュテーブルのバケットを「ベストエフォート」で走査し、走査中に見つかったキーを返します。
この挙動の結果、以下のような事象が発生する可能性があります。
- 失われたキー (Lost keys): 走査を開始した後に新しく追加されたキーや、まだ走査していないバケットから別のバケットに移動したキー(Rehash中に発生)は、その走査全体を通して一度も返されない可能性があります。
- 重複キー (Duplicated keys): 走査中にキーが別のバケットに移動した場合(Rehash)、同じキーが複数回返される可能性があります。
これらの事象は、特にキーの追加や削除、あるいはRedisのRehash(キーの数が増えたり減ったりしたときに発生するハッシュテーブルのサイズ変更)が頻繁に行われる、稼働中のシステムでSCAN
を実行した場合に発生しやすくなります。
したがって、SCAN
コマンドで取得したキーリストは、「特定の時点での全キーの正確なスナップショット」としては利用できません。SCAN
の結果を利用するアプリケーションは、以下の点を考慮する必要があります。
- 処理対象のキーが重複して含まれていても問題ないように冪等性(Idempotency)を持たせる。
- 走査中に失われたキーがあっても、後続の処理に致命的な影響がないようにする。
- 必要であれば、走査完了後に別の方法(例えば、処理済みのキーセットを保持するなど)で漏れや重複を検出・補正する仕組みをアプリケーション側で実装する。
多くの一般的なユースケース(例:一括削除、統計情報の収集など)では、一時的な重複や少数のキーの漏れが許容されるため、SCAN
コマンドは十分に実用的です。しかし、厳密な一貫性が必要な場面(例:全キーを正確にバックアップし、完全に復元する)では、SCAN
コマンド単体では不十分であり、RDBファイルやAOFファイルの解析といった別の手段を検討する必要があるかもしれません。
MATCH
オプションの挙動
MATCH pattern
オプションは、Redisサーバー側でパターンマッチングを実行し、一致するキーのみをクライアントに返すようにフィルタリングを行います。
しかし、このフィルタリングもベストエフォートです。SCAN
はまずハッシュテーブルのバケットを走査し、そのバケットに含まれるキーを取得します。その後、取得したキーに対してMATCH
パターンが適用されます。
重要な点は、COUNT
オプションで指定した数に関わらず、MATCH
パターンに一致するキーが少ない場合、SCAN
コマンド一回の呼び出しで返されるキーの数が非常に少なくなる可能性があるということです。例えば、COUNT 1000
と指定しても、走査したバケットの中に指定パターンに一致するキーが1つもなければ、空のリストが返されることがあります。この場合でも、サーバーはCOUNT
で指定された量の内部的な走査作業は行っています。
逆に、COUNT
値を小さく設定しすぎると、MATCH
パターンに一致するキーを見つけるまでに何度もSCAN
コマンドを実行する必要があり、走査全体の完了までにかかる時間が増加する可能性があります。
また、MATCH
パターンを指定しても、サーバーが走査するバケットの総数は変わりません(全てのバケットを少なくとも一度は走査する必要があります)。MATCH
はあくまで「クライアントに返す前にサーバー側でフィルタリングを行う」機能であり、特定のパターンに一致するキーだけが格納されているバケットを効率的に見つけ出すような機能ではありません。したがって、MATCH
オプションを使っても、走査全体の完了までにかかる時間は、データベース内のキーの総数やCOUNT
値、そしてパターンに一致するキーの分布に依存します。
COUNT
オプションの挙動
COUNT count
オプションは、一度のSCAN
コマンドの呼び出しでサーバーが走査する要素数(またはバケット数)のヒントです。これは「必ずcount
個のキーを返す」という意味ではありません。
- 返されるキーの数は、
COUNT
で指定した値よりも多くなることも、少なくなることもあります。特にMATCH
パターンを指定している場合や、走査しているバケットに要素が少ない場合は、返されるキー数がCOUNT
値を大きく下回ることが一般的です。 COUNT
値を大きくすると、一度のSCAN
呼び出しでより多くの内部処理が行われ、より多くのキーが返される可能性が高まります。これにより、走査全体を完了するために必要なSCAN
呼び出しの回数を減らすことができます。しかし、一度のSCAN
にかかるサーバーの処理時間が増加するため、サーバー負荷が一時的に高まる可能性があります。COUNT
値を小さくすると、一度のSCAN
呼び出しにかかるサーバーの処理時間は短くなりますが、走査全体を完了するために必要なSCAN
呼び出しの回数が増加します。これにより、クライアントとサーバー間のネットワークラウンドトリップが増え、走査全体の完了までにかかる時間は長くなる傾向があります。
適切なCOUNT
値は、サーバーの負荷状況、ネットワーク環境、クライアント側の処理能力、そしてデータベース内のキー数や分布によって異なります。一般的には、デフォルト値の10よりも大きな値(例えば100や1000)を指定することが多いですが、サーバーの応答時間を監視しながら最適な値を調整する必要があります。大量のキーを扱う本番環境では、サーバーのCPU使用率やレイテンシーをモニタリングしながら、サーバーに過大な負荷をかけない範囲でCOUNT
値を調整することが重要です。
関連コマンド
SCAN
コマンドは、キー空間全体を走査するためのコマンドですが、Redisの他のデータ構造(ハッシュ、セット、ソート済みセット)の要素を走査するための類似コマンドも存在します。これらはSCAN
コマンドと非常に似た構文と挙動を持ちます。
HSCAN key cursor [MATCH pattern] [COUNT count]
: 指定したハッシュ(Hash)型のキーに含まれるフィールドと値を走査します。戻り値は[cursor, [field1, value1, field2, value2, ...]]
という形式になります。SSCAN key cursor [MATCH pattern] [COUNT count]
: 指定したセット(Set)型のキーに含まれるメンバーを走査します。戻り値は[cursor, [member1, member2, ...]]
という形式になります。ZSCAN key cursor [MATCH pattern] [COUNT count]
: 指定したソート済みセット(Sorted Set)型のキーに含まれるメンバーとスコアを走査します。戻り値は[cursor, [member1, score1, member2, score2, ...]]
という形式になります。
これらのコマンドも、SCAN
と同様にイテレータベースであり、ノンブロッキングです。一度に大量の要素を取得することなく、カーソルを使って少しずつ走査を進めるため、大規模なハッシュ、セット、ソート済みセットを扱う際にもサーバーに過度な負荷をかけずに要素を走査することができます。MATCH
やCOUNT
オプションの挙動、そして走査中の一貫性の欠如といった特性も、SCAN
コマンドと共通しています。
SCANコマンドの安全な利用方法
SCAN
コマンドがKEYS
コマンドよりも安全である理由は、そのノンブロッキングなイテレータベースの設計にあります。一度のSCAN
呼び出しがサーバーを長時間ブロックしないため、Redisの応答性を維持したままキー走査を行うことができます。ただし、SCAN
コマンドを安全かつ効果的に利用するためには、いくつかの考慮事項とベストプラクティスがあります。
なぜSCANは安全なのか?
繰り返しになりますが、SCAN
が安全とされるのは以下の理由からです。
- ノンブロッキング:
SCAN
コマンド自体は、非常に短い時間で実行を完了します。ハッシュテーブルの少量だけを走査し、次のカーソルを計算して返すという処理は、キーの総数に比例した時間がかからず、O(1)
に近い(厳密には走査するバケット数に依存しますが、COUNT
値を適切に設定すれば短い時間で済みます)計算量で実行できます。これにより、Redisのメインスレッドが長時間停止することを避けられます。 - 少量ずつ処理: クライアントは一度に少量のキーを取得するため、クライアント側のメモリ消費も抑えられます。また、ネットワーク帯域幅も一度に大量に消費することはありません。
実際のコードでの実装方法(ループ処理)
SCAN
コマンドを利用する際は、カーソルが0
になるまでループ処理を行う必要があります。多くのRedisクライアントライブラリは、このループ処理を隠蔽するラッパー関数を提供しています。しかし、内部的な挙動を理解するために、基本的なループ構造を擬似コードで示します。
“`python
import redis
r = redis.Redis(…)
cursor = ‘0’
while True:
# SCANコマンドを実行
# COUNTは例として1000を指定
response = r.scan(cursor, match=’mykey:*’, count=1000)
# 応答の最初の要素は次のカーソル
cursor = response[0]
# 応答の2番目の要素は取得したキーのリスト
keys = response[1]
# 取得したキーリストを処理
for key in keys:
print(f"Found key: {key.decode('utf-8')}")
# ここでキーに対する操作(GET, DEL, UNLINKなど)を実行する
# 次のカーソルが '0' になったら走査完了
if cursor == '0':
break
print(“Scan complete.”)
“`
この例では、Pythonのredis-py
ライブラリを想定しています。ほとんどの言語のRedisクライアントライブラリは、scan
という名前のメソッドを提供しており、内部で上記のループ処理を自動的に行ってくれる場合があります。しかし、細かい制御(例えば、一定数のキーを処理したら一度停止するなど)を行いたい場合は、上記のような低レベルなカーソル操作を自分で実装する必要があるかもしれません。
重要なのは、返されたカーソル値を忘れずに次の呼び出しに渡すこと、そしてカーソル値が0
になるまでループを続けることです。
COUNT
オプションの適切な設定方法
COUNT
オプションの値は、パフォーマンスとサーバー負荷のバランスに影響します。
- 大きすぎる
COUNT
値: 一度のSCAN
呼び出しでより多くの処理が行われるため、その分だけRedisサーバーのCPUを占有する時間が増えます。これにより、SCAN
コマンド自体は完了までにかかる時間が短くなる可能性がありますが、その一瞬の負荷が高くなり、他のコマンドの応答時間に影響を与える可能性があります。非常に大きな値を設定すると、KEYS
コマンドほどではないにしても、無視できないレイテンシースパイクを引き起こすリスクがあります。 - 小さすぎる
COUNT
値: 一度のSCAN
呼び出しにかかるサーバーの処理時間は短くなりますが、より多くの呼び出しが必要になります。これにより、クライアントとサーバー間のネットワークラウンドトリップが増え、走査全体にかかる時間が増加します。また、大量のSCAN
呼び出しが頻繁に発行されることで、サーバーのコマンド処理キューを圧迫したり、クライアント側のCPU使用率が高くなったりする可能性があります。
最適なCOUNT
値は、Redisサーバーのスペック、現在の負荷、ネットワーク環境、キーの総数と分布など、様々な要因によって異なります。一般的には、デフォルト値の10よりも大きく、しかしサーバーが短時間で応答できる範囲の値が推奨されます。例えば、数百から数千程度がよく使われる範囲です。
本番環境でSCAN
を実行する際は、以下の点を考慮してCOUNT
値を調整してください。
- サーバーのCPU使用率:
SCAN
実行中にRedisサーバーのCPU使用率が急激に上昇しないか監視します。 - Redisのレイテンシー:
SCAN
実行中にRedisサーバーの応答時間(INFO latency
やRedis Slow Logなどを確認)が増加しないか監視します。 - ネットワーク帯域: 一度に大量のキー名を転送することでネットワーク帯域が逼迫しないか考慮します。キー名が長い場合やキー数が多い場合は注意が必要です。
- 走査にかかる総時間:
COUNT
値を調整しながら、目的の走査が現実的な時間内に完了するかを確認します。
一般的には、COUNT
値を大きくすると走査は早く終わりますが、サーバー負荷のピークが高くなります。COUNT
値を小さくすると走査はゆっくりになりますが、サーバー負荷は分散され、ピークは低くなります。ご自身の環境と要件に合わせて、トレードオフを考慮して調整を行ってください。最初は控えめな値から始め、サーバーの状況を見ながら徐々に上げていくのが安全です。
サーバー負荷を考慮した走査速度の調整
COUNT
値の調整に加えて、クライアント側でSCAN
コマンドの呼び出し間隔を調整することも、サーバー負荷を軽減する有効な手段です。例えば、一度のSCAN
コマンドを実行し、返されたキーを処理した後、次のSCAN
コマンドを実行する前に短い遅延(例えば数十ミリ秒や数百ミリ秒)を挿入します。
“`python
import redis
import time
r = redis.Redis(…)
cursor = ‘0’
while True:
response = r.scan(cursor, match=’mykey:*’, count=500)
cursor = response[0]
keys = response[1]
for key in keys:
# キーの処理...
pass # 例として何もしない
# サーバー負荷を軽減するために遅延を挿入
time.sleep(0.05) # 50ミリ秒待つ
if cursor == '0':
break
print(“Scan complete.”)
“`
このような遅延を挿入することで、クライアントがRedisサーバーに対して連続的にSCAN
コマンドを発行することを防ぎ、サーバーに処理の「息継ぎ」をする時間を与えられます。特に、クライアント側の処理がRedisからの応答を受け取る速度よりも遅い場合に有効です。遅延の時間は、サーバーの負荷状況や必要な走査速度に応じて調整します。
複数のクライアントが同時にSCANを実行する場合
複数のクライアントが同じRedisインスタンスに対して同時にSCAN
コマンドを実行することも可能です。SCAN
コマンドはそれぞれのクライアントに対して独立したカーソルを維持するため、互いの走査を妨げることはありません。
ただし、複数のSCAN
プロセスが同時に実行されると、それぞれのSCAN
がサーバーのCPUリソースを消費するため、サーバー全体の負荷は当然増加します。大量のSCAN
プロセスが同時に実行された場合、単一のSCAN
よりも高い負荷がかかり、他のコマンドのレイテンシーに影響を与える可能性があります。複数のクライアントからSCAN
を実行する必要がある場合は、それぞれのCOUNT
値を小さく設定したり、実行タイミングをずらしたりするなど、全体の負荷分散を考慮する必要があります。
走査中にキーが変更されることへの対応
前述のように、SCAN
は走査中の一貫性を保証しません。アプリケーション側で、取得したキーリストに対して操作を行う際に、以下の点を考慮する必要があります。
- 重複処理の可能性: 同じキーが複数回返される可能性があるため、キーに対する操作は冪等であるべきです。例えば、キーを削除する操作(
DEL
/UNLINK
)は冪等です。キーの値を更新する場合などは、バージョンチェックを行うなどの工夫が必要かもしれません。 - 漏れの可能性: 走査中に新しく追加されたキーや、Rehashによって走査済みのバケットから未走査のバケットに移動したキーは、今回の走査では見つからない可能性があります。全てのキーに対して操作を行う必要がある場合は、
SCAN
の結果だけでなく、別の方法(例えば、アプリケーション側で追加されたキーを別途記録しておくなど)と組み合わせるか、厳密なスナップショット取得が必要な別の手段(RDBファイル解析など)を検討する必要があります。
多くの運用タスク(例: 一括削除)では、多少の重複や漏れが許容されるため、SCAN
は非常に実用的です。しかし、もし完璧なリストアップが必要な場合は、SCAN
の限界を理解し、代替手段を検討するか、アプリケーション側で追加の検証や補正ロジックを実装することが重要です。
実用的な利用例
SCAN
コマンドは、様々な運用および開発タスクで役立ちます。ここではいくつかの具体的な利用例を紹介します。
特定のプレフィックスを持つキーの一括削除
最も一般的なユースケースの一つが、特定のプレフィックスを持つ古いキーや不要なキーを定期的に削除することです。
“`python
import redis
import time
r = redis.Redis(…)
prefix = “old_session:” # 削除したいキーのプレフィックス
cursor = ‘0’
deleted_count = 0
print(f”Scanning and deleting keys with prefix: {prefix}”)
while True:
# SCANでプレフィックスに一致するキーを少量ずつ取得
# UNLINKは非同期削除でRedisをブロックしにくいので安全
response = r.scan(cursor, match=f'{prefix}*’, count=500)
cursor = response[0]
keys_to_delete = response[1]
if keys_to_delete:
# 取得したキーを一括削除(UNLINKまたはDEL)
# UNLINKはRedis 4.0+で推奨される非同期削除
# DELは同期削除だが、リストが小さければ問題ない
try:
# UNLINKが利用可能ならUNLINKを使う
if hasattr(r, 'unlink'):
deleted_count += r.unlink(*keys_to_delete)
else:
# それ以外はDELを使う
deleted_count += r.delete(*keys_to_delete)
print(f"Deleted {len(keys_to_delete)} keys in this batch. Total deleted: {deleted_count}")
except Exception as e:
print(f"Error deleting keys: {e}")
# エラー発生時の処理(ロギング、リトライなど)
# サーバー負荷軽減のために遅延
time.sleep(0.01) # 10ミリ秒待つ
if cursor == '0':
break
print(f”Scan and delete complete. Total keys deleted: {deleted_count}”)
“`
この例では、SCAN
でキーを少量ずつ取得し、取得したリストに対してUNLINK
(またはDEL
)コマンドを使って一括削除を行っています。UNLINK
はRedis 4.0で導入されたコマンドで、キーを非同期で削除するため、大量のキーを削除する際にもRedisサーバーを長時間ブロックしないため、SCAN
と組み合わせて利用するのに非常に適しています。DEL
も少量ずつ実行すれば問題ありませんが、キーのメモリ解放に時間がかかる場合はUNLINK
がより安全です。
特定のパターンに一致するキーの値の取得と処理
特定の条件を満たすキーの値を取得して、集計や加工などの処理を行う場合にもSCAN
が利用できます。
“`python
import redis
r = redis.Redis(…)
pattern = “user_events:*:login” # 例: ユーザーログインイベントのキー
cursor = ‘0’
print(f”Scanning keys matching pattern: {pattern}”)
while True:
response = r.scan(cursor, match=pattern, count=100)
cursor = response[0]
keys = response[1]
if keys:
# 取得したキーリストに対して処理を実行
# 例: 各キーの値をGETして表示
for key in keys:
key_str = key.decode('utf-8')
try:
value = r.get(key)
if value:
print(f"Key: {key_str}, Value: {value.decode('utf-8')}")
else:
# キーが走査中に削除された場合など
print(f"Key not found or deleted during scan: {key_str}")
except Exception as e:
print(f"Error processing key {key_str}: {e}")
# 遅延を挿入
# time.sleep(0.005) # 必要に応じて
if cursor == '0':
break
print(“Scan complete.”)
“`
この例では、SCAN
で取得したキー名を使って、個別にGET
コマンドを実行しています。取得したキーに対する操作は、GET
だけでなく、HGETALL
, SMEMBERS
, ZSCAN
など、キーの型に応じた様々なコマンドが考えられます。ここでも、走査中にキーが削除されたり、値が変更されたりする可能性があるため、エラーハンドリングや処理の冪等性を考慮することが重要です。
データの移行やバックアップのためのキーリスト生成
あるRedisインスタンスから別のインスタンスへデータを移行する際や、特定のデータセットをバックアップする際に、移行/バックアップ対象となるキーのリストを生成するためにSCAN
を利用できます。
“`python
import redis
r = redis.Redis(…)
pattern = “migrate_data:*” # 移行したいキーのパターン
cursor = ‘0’
migratable_keys = []
print(f”Scanning keys for migration with pattern: {pattern}”)
while True:
response = r.scan(cursor, match=pattern, count=2000)
cursor = response[0]
keys = response[1]
if keys:
# 取得したキーリストをリストに追加
migratable_keys.extend([key.decode('utf-8') for key in keys])
print(f"Found {len(keys)} keys in this batch. Total found: {len(migratable_keys)}")
if cursor == '0':
break
print(f”Scan complete. Total keys found: {len(migratable_keys)}”)
migratable_keys リストを使って移行処理などを実行
例: パイプラインを使って別のRedisインスタンスにMOVEまたはDUMP/RESTORE
“`
この例では、SCAN
で取得したキー名をリストに格納しています。リストが構築された後、このリストを使って実際のデータ移行処理(例えば、DUMP
コマンドで値を取得し、別のインスタンスでRESTORE
するなど)を実行します。この用途では、重複は許容されますが、漏れは避けたいケースが多いかもしれません。厳密性が求められる場合は、やはりRDBファイルやAOFファイルの解析を検討する方が適切かもしれません。
期限切れが近いキーの監視
RedisのEXPIRE
やTTL
コマンドと組み合わせて、有効期限が近いキーや既に期限切れになったキーを監視・クリーンアップするためにSCAN
を利用することも考えられます。ただし、キーの有効期限は動的に変化するため、SCAN
によるリストアップはその時点でのスナップショットに過ぎないことに注意が必要です。
“`python
import redis
import time
r = redis.Redis(…)
cursor = ‘0’
expired_or_nearly_expired_keys = []
print(“Scanning keys to check TTL…”)
while True:
# 全てのキーをスキャン(MATCH ‘*’ は指定しない)
response = r.scan(cursor, count=1000)
cursor = response[0]
keys = response[1]
if keys:
for key in keys:
key_str = key.decode('utf-8')
try:
# TTLを取得
ttl = r.ttl(key)
# TTLが短い(例: 60秒以内)か、既に期限切れ(-2)か
if ttl is not None and (ttl < 60 or ttl == -2):
expired_or_nearly_expired_keys.append((key_str, ttl))
print(f"Found key with TTL {ttl}: {key_str}")
except Exception as e:
print(f"Error checking TTL for key {key_str}: {e}")
# 遅延を挿入
time.sleep(0.02) # 20ミリ秒待つ
if cursor == '0':
break
print(f”Scan complete. Found {len(expired_or_nearly_expired_keys)} expired or nearly expired keys.”)
取得したキーリストに対して追加の処理を実行(例: 削除、ロギングなど)
“`
この例では、全てのキーをSCAN
し、各キーに対してTTL
コマンドを実行して有効期限を確認しています。TTLが特定のしきい値を下回るキーをリストアップしています。ただし、Redisはバックグラウンドで期限切れキーを削除するため、TTL
が-2(期限切れ)と表示されても、次回のアクセス時には既に削除されている可能性があります。また、大量のキーに対して個別にTTL
コマンドを実行するのは、それ自体がサーバー負荷になる可能性もあるため、注意が必要です。より効率的な方法としては、Redis Enterpriseのモジュールや、RedisGearsのようなフレームワークを利用してサーバー側で処理を行うなどが考えられます。
パフォーマンスに関する考慮事項
SCAN
コマンドはノンブロッキングですが、全く負荷がかからないわけではありません。安全に利用するためには、パフォーマンスに関する以下の考慮事項を理解しておく必要があります。
- SCANコマンド自体のサーバー負荷 (CPU):
SCAN
コマンドが実行されるたびに、Redisサーバーは内部ハッシュテーブルを走査し、パターンマッチングを行い、結果を組み立ててクライアントに返します。この処理はCPUリソースを消費します。COUNT
値を大きくしたり、MATCH
パターンが複雑だったりすると、一度のSCAN
呼び出しにかかるCPU時間が増加します。複数のSCAN
プロセスが同時に実行されたり、他のCPU負荷の高いコマンド(例: 複雑なLUAスクリプト、Redis Moduleの処理など)と競合したりすると、サーバー全体のCPU使用率が上昇し、他のコマンドのレイテンシーに影響を与える可能性があります。 COUNT
値の影響: 前述の通り、COUNT
値はサーバー負荷と走査にかかる総時間のトレードオフです。高いCOUNT
値はピーク負荷を上げ、低いCOUNT
値は総時間を長くします。本番環境では、サーバーのCPU使用率や応答時間を監視しながら、許容できる範囲でCOUNT
値を設定することが重要です。MATCH
オプションの使用:MATCH
オプションはサーバー側でフィルタリングを行いますが、フィルタリングの前にハッシュテーブルのバケットを走査する基本的なコストは変わりません。また、パターンに一致するキーが少ない場合でも、COUNT
で指定した量の内部走査は行われます。したがって、MATCH
オプションを指定しても、SCAN
コマンドのCPU負荷が劇的に低くなるわけではありません。ただし、クライアントへのデータ転送量は減らせるため、ネットワーク帯域幅の節約にはなります。- ネットワーク帯域幅:
SCAN
コマンドは、取得したキー名リストをクライアントに転送します。キー名が長い場合や、一度のSCAN
呼び出しで大量のキーが返されるようにCOUNT
値を大きく設定した場合、ネットワーク帯域幅を比較的多く消費します。特に、クライアントとサーバーが異なるネットワークにいる場合や、ネットワーク帯域が限られている環境では、ネットワーク転送がボトルネックになる可能性も考慮する必要があります。 - クライアント側の処理速度:
SCAN
コマンドはサーバーから少量のキーを繰り返し取得し、クライアント側でそれらのキーに対する処理を行います。クライアント側の処理速度が遅い場合、次のSCAN
コマンドを発行するまでの間隔が長くなり、走査全体の完了までにかかる時間が増加します。また、クライアント側のCPUやメモリがボトルネックになる可能性もあります。クライアント側の処理が重い場合は、複数のクライアントで並行して走査を行う(ただしサーバー負荷に注意)、または取得したキーリストをキューに入れて別のワーカープロセスで処理するなど、クライアント側の負荷分散も検討が必要です。 - RedisのRehash: Redisの内部ハッシュテーブルは、キーの増減に応じて動的にサイズが変更されます(Rehash)。Rehash中も
SCAN
は動作しますが、ハッシュテーブルの構造が変化するため、ロストキーや重複キーが発生しやすくなります。また、Rehash処理自体もサーバーリソースを消費するため、SCAN
とRehashが同時に実行されると、一時的にサーバー負荷が高まる可能性があります。
SCAN利用時の注意点とベストプラクティス
SCAN
コマンドを安全かつ効果的に利用するために、以下の注意点とベストプラクティスを遵守することをお勧めします。
- 一貫性を求められる処理には不向き:
SCAN
は走査中の一貫性を保証しません。走査中にキーの追加、削除、変更が発生すると、結果に漏れや重複が生じる可能性があります。厳密なスナップショットが必要な処理(例: 精密なバックアップ)には、SCAN
単体ではなく、RDB/AOFファイルの解析など、他の手段を検討してください。 - 走査の完了には時間がかかる可能性:
SCAN
は一度に少量のキーしか取得しないため、データベース全体の走査には、キーの総数やCOUNT
値、クライアントの処理速度、サーバー負荷などに応じて時間がかかる場合があります。特に、大量のキー(数億個以上)を走査する場合、数時間以上かかることも珍しくありません。走査にかかる時間を事前に見積もり、許容できるか確認してください。 COUNT
値のチューニングは重要:COUNT
値はサーバー負荷と走査速度のトレードオフです。本番環境で実行する際は、最初は控えめな値から始め、サーバーのCPU使用率、レイテンシー、ネットワーク帯域などを監視しながら、最適な値を慎重に調整してください。一般的には、数百から数千程度がよく使われます。- サーバーの状態を監視しながら実行: 大規模な
SCAN
を実行する際は、Redisサーバーのメトリクス(CPU使用率、メモリ使用率、ネットワークI/O、レイテンシー、接続クライアント数など)をリアルタイムで監視することが必須です。サーバー負荷が許容範囲を超えた場合は、SCAN
プロセスの速度を落とす(COUNT
値を小さくする、遅延を入れる)か、一時停止することを検討してください。 - 本番環境での大規模なSCANは慎重に: 特にデータ量が多い本番環境で、初めて大規模な
SCAN
を実行する場合は、必ず事前にステージング環境などで十分なテストを行い、影響を評価してください。可能であれば、メンテナンスウィンドウ中に実行するか、レプリカインスタンスに対して実行することを検討してください(ただし、レプリカへのSCAN
もプライマリに負荷を与える可能性があるため注意が必要です)。 - 代替手段の検討: もし
SCAN
の一貫性の欠如が許容できない場合、あるいはSCAN
による負荷が問題となる場合は、RDBファイルやAOFファイルの解析、またはRedisGearsのようなサーバー側でのデータ処理フレームワークを利用するなど、他の手段を検討する価値があります。RDBファイルは特定の時点での正確なデータスナップショットを提供し、オフラインで解析できるため、Redisサーバーへの負荷をほとんどかけずに全キーを取得できます。 - クライアント側のリソース消費にも注意: 大量のキーを取得・処理する場合、クライアント側のCPU、メモリ、ネットワークリソースも消費されます。クライアント側の処理がボトルネックにならないように、適切なハードウェアリソースを割り当て、処理効率を最適化することも重要です。
まとめ
RedisのSCAN
コマンドは、KEYS
コマンドが抱えるブロッキングの問題を解決するために導入された、安全なキー走査のための必須コマンドです。イテレータベースでノンブロッキングな設計により、Redisサーバーの可用性を維持しながら、キー空間全体や各種データ構造の要素を少しずつ走査することを可能にします。
SCAN
コマンドの基本的な使い方、カーソルによる走査の仕組み、そしてMATCH
やCOUNT
といったオプションの挙動を理解することは、安全なキー走査を実践する上で不可欠です。また、HSCAN
, SSCAN
, ZSCAN
といった関連コマンドも同様の原則に基づいており、大規模なハッシュ、セット、ソート済みセットを扱う際に役立ちます。
ただし、SCAN
コマンドは走査中の一貫性を保証しないという重要な特性があります。これは、走査中にキーの追加・削除・変更が発生すると、結果に漏れや重複が生じる可能性があることを意味します。この点を理解し、必要に応じてアプリケーション側で対応策を講じるか、より厳密なスナップショットが必要な場合はRDB/AOFファイルの解析といった代替手段を検討する必要があります。
安全かつ効果的にSCAN
を利用するためには、COUNT
値の適切な設定、クライアント側での遅延挿入、そしてRedisサーバーのCPU使用率やレイテンシーなどの監視が非常に重要です。本番環境での大規模なSCAN
実行は慎重に行い、事前に十分なテストと評価を行うことを強く推奨します。
SCAN
コマンドを適切に使いこなすことは、Redisを用いたシステムの安定運用において非常に重要なスキルです。本記事が、あなたがRedisのSCAN
コマンドを深く理解し、日々の運用や開発タスクで安全に活用するための一助となれば幸いです。