【完全ガイド】Redis SCANの使い方、オプション、注意点を解説


【完全ガイド】Redis SCANの使い方、オプション、注意点を徹底解説

はじめに:なぜ今、RedisのSCANコマンドを学ぶべきなのか?

Redisは高速なインメモリデータストアとして、キャッシュ、セッションストア、メッセージキューなど、多岐にわたる用途で利用されています。多くのアプリケーションでRedisが利用されるにつれて、保存されるキーの数も膨大になる傾向があります。数万、数十万、あるいは数億といったキーを扱うことも珍しくありません。

このような環境で、Redisに保存されているキーの一覧を取得したり、特定のパターンにマッチするキーを探したりする必要が生じることがあります。多くの人が最初に思いつくコマンドはKEYSでしょう。

KEYS pattern

このコマンドは、指定したパターンにマッチする全てのキーを返します。開発やデバッグ目的であれば非常に便利です。しかし、プロダクション環境でキー数が大量にある場合、KEYSコマンドを使用することは極めて危険です。

なぜなら、KEYSコマンドはRedisサーバーが持っている全てのキーを(パターンマッチングしながら)単一スレッドで走査し、全ての処理が完了するまで他のコマンドの実行をブロックしてしまうからです。キーの数が多ければ多いほど、この走査に時間がかかり、その間、Redisサーバーは完全に停止した状態(フリーズ)になります。これは、高可用性や低レイテンシが求められるプロダクション環境においては、許容できない事態を引き起こす可能性があります。アプリケーションからのリクエストが応答せず、タイムアウトエラーが発生するなど、サービス全体の停止につながりかねません。

このKEYSコマンドのブロッキング問題を解決するために導入されたのが、SCANコマンドです。SCANコマンドは、Redis 2.8以降で利用可能な、非ブロッキングなカーソルベースの反復コマンドです。SCANを使用することで、Redisサーバーをブロックすることなく、安全にキー空間を走査し、キーを取得することができます。

本記事では、このRedisのSCANコマンドについて、その基本的な使い方から、豊富なオプション、そして使用する上での注意点までを、約5000語のボリュームで徹底的に解説します。SCANを正しく理解し、活用することは、Redisを大規模環境で安全に運用するために不可欠です。

SCANコマンドの基本:構文と戻り値

SCANコマンドは、Redisのキー空間全体を、一度に全てではなく、少しずつ反復して取得するために設計されています。その基本的な構文は以下の通りです。

SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]

  • cursor: 必須引数です。反復処理の状態を示すカーソル値を指定します。最初の呼び出しでは0を指定します。その後の呼び出しでは、前回のSCANコマンドの戻り値として得られたカーソル値を指定します。
  • MATCH pattern: オプションです。指定したglob形式のパターンにマッチするキーのみを返そうとします。デフォルトは*(全てのキー)です。
  • COUNT count: オプションです。一度の呼び出しで処理する要素数に関するヒントを指定します。実際に返される要素数はこの値と異なる場合があります。デフォルトは10です。
  • TYPE type: オプションです。指定した型のキーのみを返そうとします。指定可能な型はstring, list, set, zset, hash, streamなど、Redisがサポートする型です。

SCANコマンドの戻り値は常に2つの要素を持つ配列(List)です。

[next_cursor, [key1, key2, ...]]

  • next_cursor: 次回のSCANコマンドで使用すべきカーソル値です。この値が0になったら、反復処理は完了です。
  • [key1, key2, ...]: 今回のSCANコマンドの実行で取得されたキーのリストです。このリストは空である可能性もあります。

基本的な使い方:カーソルを使ったループ処理

SCANコマンドは一度の呼び出しで全てのキーを返すわけではありません。カーソル値をやり取りしながら複数回コマンドを実行することで、キー空間全体を少しずつ走査していきます。この処理は、一般的にループとして実装されます。

基本的な処理の流れは以下のようになります。

  1. 初期カーソル値を0としてSCANコマンドを実行します。
    例: SCAN 0
  2. コマンドの戻り値として、次のカーソル値とキーのリストを取得します。
  3. 取得したキーリストを処理します(表示、保存、他のコマンドの実行など)。
  4. 取得した次のカーソル値が0であれば、反復処理は終了です。
  5. 次のカーソル値が0以外であれば、その値を新たなカーソルとして、再度SCANコマンドを実行します。
  6. 3-5の手順を、カーソル値が0になるまで繰り返します。

擬似コードで表現すると以下のようになります。

“`python
cursor = 0
while True:
# SCANコマンドを実行
# 例: scan_result = redis_client.scan(cursor=cursor, match=’user:*’, count=100)
# scan_result は [next_cursor, [key1, key2, …]] という形式
next_cursor, keys = execute_scan_command(cursor, match_pattern, count_hint, type_filter)

# 取得したキーを処理
for key in keys:
    process_key(key)

# 次のカーソル値を取得
cursor = int(next_cursor) # Redisクライアントライブラリによっては整数で返される

# カーソルが0なら終了
if cursor == 0:
    break

print(“SCAN process finished.”)
“`

多くのRedisクライアントライブラリは、このカーソルを使ったループ処理をより簡単に実行できるラッパー関数(イテレータやジェネレータ)を提供しています。例えば、Pythonのredis-pyライブラリでは、scan_iter()メソッドが利用できます。

“`python
import redis

Redisサーバーに接続

r = redis.Redis(host=’localhost’, port=6379, db=0)

print(“Starting SCAN…”)

scan_iter()はジェネレータを返すため、forループでキーを順次取得できる

countやmatchオプションも指定可能

for key in r.scan_iter(match=’user:*’, count=1000):
# key はバイト列で返されるのでデコードが必要な場合が多い
decoded_key = key.decode(‘utf-8’)
print(f”Found key: {decoded_key}”)

print(“SCAN process finished.”)
“`

このように、クライアントライブラリの提供するメソッドを利用することで、開発者はカーソル管理の詳細を意識することなく、簡潔にSCAN処理を記述できます。しかし、SCANの仕組みと注意点を理解するためには、カーソルベースのループ処理の概念を把握しておくことが重要です。

SCANコマンドの仕組み:カーソルベース反復の裏側

SCANコマンドが非ブロッキングである理由は、Redisの内部データ構造とカーソルベースの反復処理にあります。

Redisのキー空間は、内部的にはハッシュテーブル(Dictionary)で管理されています。このハッシュテーブルは複数の「バケット(Bucket)」と呼ばれる区画に分割されています。SCANコマンドのカーソル値は、この内部的なハッシュテーブルの構造(特に、リハッシュ中の古いハッシュテーブルと新しいハッシュテーブルの両方、そして各バケット)を走査する際の状態を示しています。

最初のカーソル0から開始すると、Redisはハッシュテーブルの特定のバケットからスキャンを開始します。COUNTオプションは、この処理でどれくらいの数のバケットを走査するか、あるいは各バケットからどれくらいの数の要素を考慮するか、といったことに対するヒントとして使用されます。Redisは指定されたCOUNT値を参考にしながら、内部的な状態を進め、いくつかのキー(MATCHTYPEでフィルタリングされうるキー)をクライアントに返します。そして、次にスキャンすべき内部的な状態を示すカーソル値を返します。

クライアントは次にSCANコマンドを実行する際に、この返されたカーソル値をそのまま使用します。Redisサーバーは、受け取ったカーソル値に基づいて、前回停止した場所からスキャンを再開します。この処理を、内部的な全てのバケットを走査し終えて、カーソル値が0に戻るまで繰り返します。

重要なのは、Redisサーバー側はクライアントごとのスキャンセッションの状態(どのキーを既に返したか、次にどこから始めるか)を積極的に管理しているわけではないということです。カーソル値は、あくまでクライアントがサーバーに「次はこのあたりからスキャンしてほしい」と伝えるための情報です。サーバーはカーソル値を元にスキャンを開始し、処理を進めて次のカーソル値を計算して返します。この仕組みのおかげで、SCANコマンドはサーバー側の状態を保持せず、軽量に実行できるため、ブロッキングしないのです。

このカーソルベースの仕組みは、スキャン中にキーが追加されたり削除されたりした場合に、全ての結果を正確に取得することを保証しないという特性をもたらします。この点については、「注意点」のセクションで詳しく解説します。

SCANコマンドのオプション詳解

SCANコマンドには、スキャン処理をより柔軟に制御するためのオプションが用意されています。それぞれのオプションについて詳しく見ていきましょう。

MATCH pattern

MATCHオプションを使用すると、指定したglob形式のパターンにマッチするキーのみを取得しようとします。

SCAN cursor MATCH pattern

  • pattern: glob形式のパターン文字列を指定します。利用可能なワイルドカードは以下の通りです。
    • *: 0文字以上の任意の文字列にマッチします。
    • ?: 任意の一文字にマッチします。
    • [abc]: 角括弧内の任意の文字にマッチします。例: [abc]は’a’、’b’、’c’のいずれかにマッチ。
    • [a-z]: 角括弧内の範囲指定された任意の文字にマッチします。例: [a-z]はアルファベット小文字のいずれかにマッチ。
    • [^abc]: 角括弧内の文字以外の任意の文字にマッチします。例: [^0-9]は数字以外の文字にマッチ。

例:

  • SCAN 0 MATCH user:*: “user:”で始まる全てのキーをスキャンします。
  • SCAN 0 MATCH session:????: “session:”で始まり、その後に正確に4文字が続くキーをスキャンします(例: “session:1234″)。
  • SCAN 0 MATCH article:[0-9]*: “article:”で始まり、その後に数字が続くキーをスキャンします。

MATCHオプションの注意点:

MATCHオプションは、取得したキーに対して後からフィルタリングを行うわけではありません。スキャン処理の内部で、指定されたパターンにマッチしそうなキーを優先的に探索しようとします。しかし、これは厳密なフィルタリングや効率化を保証するものではありません

例えば、SCAN 0 MATCH *は事実上SCAN 0と同じで、全てのキーをスキャンします。SCAN 0 MATCH prefix:*のようにプレフィックスを指定した場合でも、Redisは内部的なハッシュテーブルのバケットを順に走査するため、指定したパターンにマッチしないキーが存在するバケットもスキャンされる可能性があります。そして、スキャンされたキーの中からパターンにマッチするものを選んでクライアントに返します。

つまり、MATCHオプションを指定しても、パターンにマッチしない多くのキーがサーバー内部で処理される可能性があります。特に、非常に多数のキーがあり、かつMATCHパターンが多くのキーにマッチする場合(例: *や、多くのキーが共通のプレフィックスを持つ場合のそのプレフィックス)、スキャン処理全体のパフォーマンスに大きな影響を与える可能性があります。しかし、特定のプレフィックスを持つキーのみが必要な場合には、ネットワーク転送量を減らせるため有効です。

MATCHオプションは、あくまでRedisがクライアントに返すキーを絞り込むためのものであり、スキャン自体の総当たり的な性質を根本的に変えるものではないと理解しておくべきです。

COUNT count

COUNTオプションは、一度のSCANコマンドの呼び出しで、Redisが内部的に処理を進める要素数に関するヒントを指定します。

SCAN cursor COUNT count

  • count: 正の整数を指定します。デフォルトは10です。

COUNTオプションの重要な理解:

最もよく誤解される点ですが、COUNT count一度に返されるキーの最大数を指定するものではありません。Redisは指定されたCOUNT値を参考に、内部的なバケットをどれだけ処理するかなどを決定します。例えば、COUNT 1000と指定した場合でも、実際に返されるキーの数は0個かもしれませんし、数個、数十個、あるいは1000個を大きく超えることもあります。これは、スキャン対象のバケットに格納されているキーの数や、MATCH/TYPEフィルタリングの結果に依存します。

COUNT値がパフォーマンスに与える影響:

COUNT値の大小は、SCANコマンドの挙動とサーバー負荷に影響します。

  • COUNT値を小さくする:

    • 一度に返されるキーの数が少なくなる傾向があります。
    • SCANコマンド自体の実行時間は短くなります。
    • クライアントとサーバー間の通信回数が増えます。
    • サーバーのCPU負荷が分散され、スパイクが発生しにくくなります。
    • ネットワーク帯域の使用量が、短時間で大量に発生するのではなく、複数回に分散されます。
  • COUNT値を大きくする:

    • 一度に返されるキーの数が多くなる傾向があります。
    • SCANコマンド自体の実行時間が長くなる可能性があります。
    • クライアントとサーバー間の通信回数が減ります。
    • 一度のSCANコマンドでサーバーのCPU使用率が高くなる可能性があります(CPU負荷のスパイク)。
    • 一度に大量のキー情報を転送するため、ネットワーク帯域を一時的に多く消費します。

適切なCOUNT値の選択:

最適なCOUNT値は、Redisサーバーの構成、ネットワーク環境、そしてSCANを実行する頻度や目的によって異なります。一般的には以下の点を考慮して決定します。

  • サーバーのCPU負荷: 小さなCOUNT値を頻繁に実行するか、大きなCOUNT値をゆっくり実行するかで、CPU負荷のパターンが変わります。低レイテンシが重要な環境では、小さなCOUNT値でCPU負荷のスパイクを避ける方が良い場合があります。
  • ネットワーク帯域: 大量のキーを返す場合、ネットワーク帯域を圧迫する可能性があります。ネットワークがボトルネックになりやすい環境では、COUNT値を調整して一度に転送するデータ量を制御することを検討します。
  • クライアント側の処理能力: 取得したキーをすぐに処理する場合、クライアント側の処理能力に合わせてCOUNT値を調整する必要があります。
  • キーの総数と分布: キー数が非常に多い場合や、特定のバケットにキーが偏っている場合など、キーの分布によっても挙動が変わることがあります。

多くのユースケースでは、デフォルトの10よりは大きな値(例: 100, 500, 1000)が使われることが多いです。これは、通信回数を減らして全体の完了時間を短縮するためです。しかし、プロダクション環境に導入する前に、実際のデータ量とサーバー負荷を考慮して、何度かテスト実行を行い、最適なCOUNT値を見つけることが推奨されます。

TYPE type

TYPEオプションを使用すると、指定したRedisのデータ型のキーのみを取得しようとします。

SCAN cursor TYPE type

  • type: 取得したいキーのデータ型を指定します。有効な型は以下の通りです(Redisのバージョンやモジュールによって増える可能性があります):
    • string
    • list
    • set
    • zset (Sorted Set)
    • hash
    • stream
    • その他、Redisモジュールが提供する型(例: ReJSON-RL, searchなど)

例:

  • SCAN 0 TYPE hash: ハッシュ型のキーのみをスキャンします。
  • SCAN 0 MATCH user:* TYPE string: “user:”で始まる文字列型のキーのみをスキャンします。

TYPEオプションの注意点:

TYPEオプションもMATCHと同様に、スキャン後にフィルタリングを行うわけではありません。Redisは内部的なスキャン処理の過程でキーの型を確認し、指定された型に一致するキーのみをクライアントに返します。

TYPEオプションを指定しても、指定された型以外のキーも内部的にはスキャンされる可能性があります。しかし、クライアントに不要なキー情報が転送されることを防げるため、ネットワーク帯域の節約には役立ちます。また、特定の型のキーのみを対象としたい場合に、クライアント側で型をチェックする手間を省くことができます。

MATCHTYPEを組み合わせて使用することで、より特定の条件に合致するキーに絞ってスキャンすることができます。

SCANコマンドの注意点:理解すべき特性

SCANコマンドはKEYSコマンドの問題を解決する優れたコマンドですが、そのカーソルベースの仕組みゆえに、いくつかの重要な特性と注意点があります。これらを理解せずに使用すると、予期せぬ結果や問題に直面する可能性があります。

1. スキャン中のキーの増減/変更への対応

SCANコマンドは、スキャン開始時点のキー空間のスナップショットを取得するわけではありません。スキャンが進行している間に、他のクライアントがキーを追加したり、削除したり、変更したりする可能性があります。

この動的なキー空間に対してSCANはどのように振る舞うのでしょうか?

  • 追加されたキー: スキャン開始後に新しく追加されたキーは、そのキーがどのバケットに割り当てられるかによりますが、スキャン処理のどこかで見つかる可能性があります。しかし、全ての新しいキーが見つかる保証はありません。スキャンが完了する前に新しいキーが追加され、かつそのキーがスキャンが既に通過したバケットに割り当てられた場合、そのキーは今回のスキャンでは見逃される可能性があります。
  • 削除されたキー: スキャン処理が特定のキーを返す前にそのキーが削除された場合、そのキーはクライアントに返されません。これは通常期待される挙動です。しかし、スキャン処理が特定のキーをクライアントに返したにそのキーが削除されても、そのキーが返されたという事実は変わりません。
  • 変更されたキー: キーの値や型が変更された場合、SCANコマンド自体はキー名を取得するだけなので直接影響はありません。ただし、TYPEオプションを使用している場合に、キーの型がスキャン中に変更されると、フィルタリングの結果が変わる可能性があります。

Redisの公式ドキュメントでは、SCANファミリーのコマンドは「最終的に全ての要素を訪問する可能性がある」と説明されています。これは、スキャン開始時点に存在した全てのキーが、スキャンが完了するまでに一度はクライアントに返される可能性が高いという意味合いで解釈できます。しかし、スキャン中に削除されたキーは返されませんし、追加されたキーは返されるかもしれませんし、返されないかもしれません。

したがって、SCANコマンドで取得したキーリストは、ある時点のキー空間を正確に反映したものではなく、ある程度のあいまいさを含む反復処理の結果であるということを理解しておく必要があります。もし、厳密に「スキャン開始時点に存在した全てのキー」が必要な場合は、別の方法(例えば、スキャン中にキーを一時的に別の構造にコピーするなど)を検討する必要がありますが、これは一般的に複雑でサーバー負荷も高くなります。多くのユースケースでは、SCANの特性を受け入れて利用します。

2. パフォーマンスへの影響

SCANKEYSのようにサーバーをブロックしませんが、全く負荷をかけないわけではありません。SCANはRedisサーバーのCPUを使用します。

  • CPU負荷:SCANコマンドの実行時、Redisは内部的なハッシュテーブルを走査し、MATCHTYPEオプションに基づいてキーをフィルタリングします。この処理はCPUリソースを消費します。COUNT値を大きくするほど、一度のSCAN呼び出しあたりのCPU使用率は高くなる傾向があります。高頻度でSCANを実行したり、複数のクライアントが同時にSCANを実行したりすると、サーバー全体のCPU使用率が上昇し、他のコマンドのレイテンシに影響を与える可能性があります。
  • ネットワーク帯域: 返されるキーの数が多い場合、ネットワーク帯域を消費します。特にCOUNT値を大きく設定した場合、一度に大量のデータが転送される可能性があります。
  • レプリケーション遅延: SCANコマンド自体はディスクI/Oを伴いませんが、マスターサーバーで実行されたSCANはレプリカに転送されません(スキャン結果ではなくコマンド自体が転送されるわけではない)。しかし、スキャン中にマスターで頻繁にキーの変更(追加、削除、更新)が発生する場合、それらの変更によるレプリケーションデータがSCANの結果送信と競合する可能性があります。また、SCANによるCPU負荷がマスターサーバーに影響を与え、結果的にレプリケーションの遅延を引き起こす可能性もゼロではありません。

プロダクション環境でSCANを使用する際には、監視ツールなどを使ってRedisサーバーのリソース使用率(CPU、ネットワーク)を注意深く監視し、必要に応じてCOUNT値を調整したり、SCANの実行頻度を制限したりといった対策を講じることが重要です。

3. アトミック性がない

SCANコマンドはアトミックな操作ではありません。つまり、複数のSCAN呼び出し全体として、キー空間のある一貫した状態を反映するものではありません。前述のように、スキャン中にキーが追加、削除、変更される可能性があるため、取得されたキーのセットは、スキャン開始から終了までの間に発生した変更の影響を受けます。

もし、ある特定の瞬間のキー空間のスナップショットが必要なのであれば、SCANは適切なツールではありません。そのような要件がある場合は、RDBスナップショットを取得するなど、別の方法を検討する必要があります。ただし、RDBスナップショットの取得も、大きなデータセットではI/O負荷を伴う可能性があるため注意が必要です。

4. タイムアウトの概念がない

SCANコマンド自体には、実行時間のタイムアウトを設定するオプションはありません。一度SCAN cursorコマンドを発行すると、Redisサーバーは内部処理を完了して次のカーソル値とキーリストを返すまで処理を続けます。

もしクライアント側でSCANの実行時間を制限したい場合は、クライアントライブラリの機能や、アプリケーションレベルでのタイムアウト処理を実装する必要があります。例えば、一定時間応答がなかったらコマンドをキャンセルする、などの処理をクライアント側で行います。

5. クライアント側のメモリ使用量

SCANコマンドは一度に全てのキーを返すわけではありませんが、一度の呼び出しで返されるキーのリストが大きくなる可能性があります(特にCOUNT値を大きく設定した場合)。クライアントアプリケーションは、この返されたキーリストをメモリに保持する必要があります。キー名が非常に長い場合や、COUNT値を極端に大きく設定した場合、クライアント側のメモリ使用量が無視できないレベルになる可能性があるので注意が必要です。

また、スキャンで見つかった全てのキーをメモリ上のリストなどに一旦保持してからまとめて処理する場合、キーの総数に比例したメモリが必要になります。もしメモリが少ない環境で大量のキーをスキャンする場合は、取得したキーをストリーム処理したり、ファイルに一時保存したりするなど、メモリ消費を抑える工夫が必要になります。

SCAN関連コマンド:SSCAN, HSCAN, ZSCAN

SCANコマンドはキー空間全体を走査するためのものですが、Redisの複合型(Set, Hash, Sorted Set)の要素を反復処理するための類似コマンドも存在します。これらはSCANと同様のカーソルベースの非ブロッキングな反復処理を提供します。

SSCAN (Setのスキャン)

Setに格納されている要素をスキャンするためのコマンドです。

SSCAN key cursor [MATCH pattern] [COUNT count]

  • key: スキャン対象のSetのキーを指定します。
  • cursor: SCANと同様のカーソル値。
  • MATCH pattern: オプション。glob形式のパターンにマッチする要素のみを返そうとします。
  • COUNT count: オプション。ヒントとして使用する処理要素数を指定します。

戻り値もSCANと同様に [next_cursor, [element1, element2, ...]] の形式です。

SSCANは、巨大なSetに含まれる特定の要素を探したり、全ての要素を列挙したりする際にSMEMBERSコマンドの代わりに安全に使用できます。SMEMBERSはSetの全要素を一度にメモリにロードして返すため、巨大なSetに対して実行するとサーバーをブロックする可能性があります。

HSCAN (Hashのスキャン)

Hashに格納されているフィールドと値をスキャンするためのコマンドです。

HSCAN key cursor [MATCH pattern] [COUNT count]

  • key: スキャン対象のHashのキーを指定します。
  • cursor: SCANと同様のカーソル値。
  • MATCH pattern: オプション。glob形式のパターンにマッチするフィールド名のみを返そうとします。
  • COUNT count: オプション。ヒントとして使用する処理要素数を指定します。

戻り値は [next_cursor, [field1, value1, field2, value2, ...]] の形式です。フィールド名と値が交互にリストされます。

HSCANは、巨大なHashに含まれる全てのフィールド・値を列挙したり、特定のフィールドパターンを持つエントリを探したりする際にHGETALLコマンドの代わりに安全に使用できます。HGETALLもHashの全エントリを一度に返すため、巨大なHashに対して実行するとサーバーをブロックする可能性があります。

ZSCAN (Sorted Setのスキャン)

Sorted Setに格納されている要素とスコアをスキャンするためのコマンドです。

ZSCAN key cursor [MATCH pattern] [COUNT count]

  • key: スキャン対象のSorted Setのキーを指定します。
  • cursor: SCANと同様のカーソル値。
  • MATCH pattern: オプション。glob形式のパターンにマッチする要素(メンバー)のみを返そうとします。
  • COUNT count: オプション。ヒントとして使用する処理要素数を指定します。

戻り値は [next_cursor, [element1, score1, element2, score2, ...]] の形式です。要素(メンバー)とスコアが交互にリストされます。スコアは文字列として返されます。

ZSCANは、巨大なSorted Setに含まれる全ての要素・スコアを列挙したり、特定の要素パターンを持つエントリを探したりする際にZRANGEコマンド(全範囲指定)の代わりに安全に使用できます。ZRANGEで全範囲を指定すると、Sorted Setの全要素を一度に返すため、巨大なSorted Setに対して実行するとサーバーをブロックする可能性があります。

これらのSSCAN, HSCAN, ZSCANコマンドは、それぞれSet、Hash、Sorted Setの内部構造をSCANと同様のカーソルベースの手法で走査します。基本的な仕組みや注意点(スキャン中の要素の増減によるあいまいさ、非ブロッキングだがCPUを使用するなど)はSCANと同様です。特定の型の要素を安全に列挙したい場合に非常に役立ちます。

SCANとその他のキー操作コマンドの比較

改めて、SCANコマンドを他のキー操作コマンドと比較し、それぞれの使い分けを明確にします。

  • KEYS pattern

    • 特性: 指定パターンにマッチする全てのキーを一度に返す。ブロッキングコマンド。
    • 用途: 開発環境、デバッグ、非常にキー数が少ない環境での利用に限定。プロダクション環境での大量キーに対する使用は避けるべき。
    • 利点: シンプルで使いやすい。全ての結果を一度に取得できる。
    • 欠点: キー数が多いとサーバーを長時間ブロックし、サービス停止につながる可能性がある。
  • SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]

    • 特性: カーソルベースの非ブロッキングな反復処理。一度に少しずつキーを返す。スキャン中のキーの増減により、結果にあいまいさが生じる可能性がある。
    • 用途: プロダクション環境で安全にキー空間を走査する場合。大量のキーを処理する場合。特定のパターン/型のキーを定期的に検索/処理する場合。
    • 利点: Redisサーバーをブロックしない。サーバー負荷を分散させながらキーを列挙できる。
    • 欠点: 全ての結果を取得するには複数回のコマンド実行とクライアント側でのカーソル管理が必要。スキャン中のキーの増減によって結果にあいまいさが生じる。全てのキーを列挙するまでに時間がかかる。

まとめると、開発やテストで手軽にキーを確認したい場合はKEYS、プロダクション環境で大量のキーを安全かつ継続的に処理したい場合はSCANを使用するのが基本となります。

実践的な利用例

SCANコマンドは、プロダクション環境でRedisを運用する上で様々な場面で活用できます。以下にいくつかの具体的な利用例を挙げます。

1. 定期的なデータクリーンアップ (古いキーの発見と削除)

有効期限が設定されていない、あるいは設定されているがシステム側の問題で適切に削除されない古いキーがRedisに残っている場合、それらがメモリを圧迫する原因となります。SCANを使って定期的にキー空間を走査し、特定の命名規則に従わないキーや、特定の条件(例: 値の内容から最終更新日時を判断)を満たす古いキーを発見し、DELコマンドで削除する処理を実装することができます。

例: “temp:”プレフィックスを持つキーをスキャンし、一定期間アクセスされていないものを削除する(実際のアクセス時間はRedisレベルでは取得できないため、アプリケーション側で最終アクセス時間を値や別のキーで管理する必要がある場合が多い)

“`python
import redis
import time

r = redis.Redis(host=’localhost’, port=6379, db=0)

仮に、キーの値にJSON形式で {“data”: …, “timestamp”: 1678886400} のように最終更新タイムスタンプが入っているとする

1週間(604800秒)以上更新されていないキーを削除

threshold_timestamp = int(time.time()) – 604800

print(f”Deleting keys older than timestamp: {threshold_timestamp}”)

for key_bytes in r.scan_iter(match=’temp:*’, count=500):
key = key_bytes.decode(‘utf-8’)
try:
# キーの型をチェック(ここではstringと仮定)
key_type = r.type(key).decode(‘utf-8’)
if key_type == ‘string’:
value = r.get(key)
if value:
try:
import json
data = json.loads(value)
if ‘timestamp’ in data and isinstance(data[‘timestamp’], (int, float)):
if data[‘timestamp’] < threshold_timestamp:
print(f”Deleting old key: {key}”)
r.delete(key)
except (json.JSONDecodeError, TypeError, KeyError):
# JSON形式でない、または必要なフィールドがない場合はスキップまたはログ記録
print(f”Skipping non-standard value for key: {key}”)
else:
# キーが取得できなかった(スキャン中に削除された可能性など)
print(f”Key {key} not found during get, potentially deleted already.”)
else:
# 想定外の型の場合はスキップまたはログ記録
print(f”Skipping key {key} with unexpected type: {key_type}”)

except Exception as e:
    print(f"Error processing key {key}: {e}")

print(“Scan and deletion process finished.”)
“`

この例では、SCANを使ってキーを列挙し、個々のキーに対してGETTYPEコマンドを実行しています。COUNT値を調整することで、サーバーへの負荷を制御できます。処理中にキーが削除されてもGETNoneが返されるため、安全に処理を続行できます。

2. 特定のパターンを持つキーの一括処理

特定のプレフィックスを持つ全てのキーに対して、値の形式を変更したり、新しいフィールドを追加したりといった一括処理を行う場合にもSCANが適しています。例えば、ユーザーデータを保存しているキーの構造を変更するマイグレーション処理などです。

例: “user:”で始まる全てのHashキーに対して、新しいフィールドを追加する

“`python
import redis
import json

r = redis.Redis(host=’localhost’, port=6379, db=0)

new_field_name = ‘status’
new_field_value = ‘active’

print(f”Adding field ‘{new_field_name}’ to ‘user:*’ hash keys…”)

for key_bytes in r.scan_iter(match=’user:*’, type=’hash’, count=1000):
key = key_bytes.decode(‘utf-8’)
try:
# HSCANを使って現在のフィールドを取得し、存在しない場合のみ追加するなどの高度な処理も可能
# ここではシンプルに HSET でフィールドを追加/更新
result = r.hset(key, new_field_name, new_field_value)
if result == 1: # 新しいフィールドが追加された場合
print(f”Added field ‘{new_field_name}’ to {key}”)
elif result == 0: # フィールドが既に存在し、値が更新された場合 (今回は同じ値なので実質変更なし)
print(f”Field ‘{new_field_name}’ already exists for {key}”)
else: # 何らかの問題
print(f”Unexpected result {result} for hset on {key}”)

except Exception as e:
    print(f"Error processing key {key}: {e}")

print(“Scan and update process finished.”)
“`

TYPE hashオプションを使うことで、Hash型のキーのみを効率的にスキャンできます。COUNT値を適切に設定し、処理頻度を調整することで、プロダクション環境に大きな影響を与えずにこのようなバッチ処理を実行できます。

3. 監視・監査

特定の種類のキーがどれくらい存在するかを定期的にカウントしたり、予期しないキーが存在しないかを確認したりといった監視や監査の目的でSCANを利用することも可能です。

例: “cache:”プレフィックスを持つキーの総数をカウントする

“`python
import redis

r = redis.Redis(host=’localhost’, port=6379, db=0)

count = 0
print(“Counting ‘cache:*’ keys…”)

scan_iter() は内部でカーソル管理を行うため、シンプルにループできる

for key_bytes in r.scan_iter(match=’cache:*’, count=2000):
count += 1

print(f”Total ‘cache:*’ keys found: {count}”)
“`

SCANはスキャン中のキーの増減に対してあいまいさがあるため、取得したcountは厳密なリアルタイムのキー数とは異なります。しかし、趨勢を把握したり、異常な増加を検知したりする目的には十分利用可能です。より厳密なキー数が必要な場合は、アプリケーション側でキーの追加/削除時に別途カウンタを管理するなどの工夫が必要になります。

4. バックアップの一部としてのキー列挙

RDBやAOFといったRedisの標準的なバックアップ機能は、特定の時点のデータセット全体を保存するためのものです。しかし、これらのファイルから特定のキーの情報だけを取り出したり、キーリストだけを取得したりするのは容易ではありません。

SCANを使用することで、Redisサーバーに接続し、アクティブなキーリストを取得することができます。このキーリストを元に、別途DUMPコマンドなどを使用して個々のキーの値をシリアライズ形式で取得し、独自のバックアップシステムを構築する際の一部として利用することが考えられます。

例: 全キー名をファイルに保存する

“`python
import redis

r = redis.Redis(host=’localhost’, port=6379, db=0)

output_filename = ‘redis_keys.txt’
print(f”Writing all keys to {output_filename}…”)

with open(output_filename, ‘w’, encoding=’utf-8′) as f:
for key_bytes in r.scan_iter(count=5000): # COUNTを大きめに設定し、I/O回数を減らす
try:
key = key_bytes.decode(‘utf-8’)
f.write(key + ‘\n’)
except Exception as e:
print(f”Error processing key {key_bytes}: {e}”)

print(“Key list saved.”)
“`

このキーリストがあれば、必要に応じて特定のキーをRESTOREコマンドで復元するといったことが可能になります。ただし、これもスキャン中のキーの増減によるあいまいさは伴います。

SCANを効果的に使うためのヒント

SCANコマンドを最大限に活用し、Redisサーバーへの影響を最小限に抑えるためのヒントをいくつか紹介します。

  1. 適切なCOUNT値を選択する: 前述のように、COUNT値はパフォーマンスに大きな影響を与えます。開発環境で試すだけでなく、本番に近い環境で様々なCOUNT値を試して、サーバーのCPU使用率、ネットワーク帯域、スキャン完了までの時間などを測定し、最適な値を見つけることが重要です。多くの場合はデフォルトの10よりも大きな値(100〜数千程度)が現実的ですが、大きすぎると一度のコマンドでサーバー負荷が急増します。
  2. MATCHパターンを可能な限り絞り込む: 必要なキーが特定のプレフィックスやサフィックスを持つことが分かっている場合、積極的にMATCHオプションを使用しましょう。これにより、クライアントに不要なキー情報が転送されるのを防ぎ、ネットワーク帯域を節約できます。ただし、MATCHがサーバー内部の総走査量を劇的に減らすわけではないことを理解しておいてください。
  3. TYPEオプションを活用する: 必要なキーの型が分かっている場合、TYPEオプションを指定することで、クライアント側での型チェックが不要になり、不要なキー情報の転送も防げます。
  4. スキャン頻度と実行時間を制御する: サーバーのピーク時間帯にリソースを大量に消費するようなSCAN処理(特に大きなCOUNT値を使用する場合)を実行するのは避けるべきです。定期的なメンテナンスジョブとして実行する場合は、サーバー負荷の低い時間帯を選択したり、スキャン処理を一時停止・再開できるような仕組みをクライアント側に実装したりすることを検討してください。
  5. スキャン結果の一時保存場所を検討する: スキャンで見つかった全てのキーやそれらの値に対して、後続の処理を行う必要がある場合、それらをどこに一時的に保存するかも重要です。クライアントのメモリ、ファイル、データベースなど、システムの要件とリソースを考慮して適切な方法を選択してください。大量のデータを扱う場合は、メモリ効率の良い方法を選ぶことが重要です。
  6. 複数のクライアントで並行してスキャンする場合の注意: 複数のクライアントが同時に同じRedisインスタンスに対してSCANを実行することは可能ですが、それぞれのクライアントは独立したカーソルを持ちます。Redisサーバーはクライアントごとのスキャン状態を管理せず、受け取ったカーソル値に基づいて処理を進めます。複数のクライアントが並行してスキャンを実行すると、それぞれのスキャン処理がサーバーのリソース(特にCPU)を分け合うことになり、個々のスキャンの完了にかかる時間は長くなる可能性があります。また、全体としてスキャンされるバケットに重複が生じる可能性も高まります。
  7. スキャン結果の不確実性を考慮した処理を設計する: 前述のように、SCANはスキャン中のキーの増減に対してあいまいさがあります。スキャンで取得したキーが存在するかどうかは、実際にそのキーに対するコマンド(例: GET, DEL)を実行するまで分かりません。したがって、スキャン結果のキーを処理する際には、「そのキーが既に存在しない可能性がある」ということを考慮して、エラーハンドリングを適切に行う必要があります。

よくある質問 (FAQ)

Q: SCANコマンドはKEYSよりも遅いのですか?

A: 厳密には異なります。KEYSは全ての結果を返すまで他のコマンドをブロックし、その間は応答しません。完了までの総時間で見れば、大量のキーがある場合は非常に長くなる可能性があります。一方、SCANは一回の呼び出しあたりの実行時間は短く、サーバーをブロックしません。しかし、全てのキーを列挙するには複数回の呼び出しが必要であり、その総実行時間KEYSよりも長くなることもあります(特にCOUNT値が小さい場合や、ネットワーク往復のレイテンシが大きい場合)。重要なのは、SCANがサーバーをブロックしないため、他のコマンドのレイテンシに与える影響が少ないという点です。

Q: SCANで取得したキーは、スキャン完了時点でも全て存在しますか?

A: いいえ。SCANでキー名がクライアントに返された後でも、他のクライアントによってそのキーが削除される可能性があります。実際にキーを操作(例: GETDEL)する際には、そのキーが存在しない可能性を考慮して処理を記述する必要があります。

Q: SCANで取得したキーは、スキャン開始時点で存在した全てのキーですか?

A: 「最終的に全ての要素を訪問する可能性がある」という意味では近いですが、厳密には違います。スキャン開始後に削除されたキーはクライアントには返されません。スキャン中に新しく追加されたキーは、返される場合と返されない場合があります。したがって、SCANの結果は、スキャン開始時点のキー空間のスナップショットでもなく、スキャン完了時点のキー空間でもなく、スキャン期間中に変動するキー空間のある程度の反映であると理解すべきです。

Q: SCANのカーソル値はサーバー側で管理されている、クライアントごとの状態ですか?

A: いいえ。Redisサーバーはクライアントごとのスキャン状態を積極的に管理していません。カーソル値は、Redisの内部的なハッシュテーブルの走査位置を示す情報であり、クライアントは前回のコマンドで受け取ったカーソル値をそのまま次回のコマンドの引数として渡します。サーバーはそのカーソル値を見て、どこからスキャンを再開するかを判断します。このステートレスな設計が、SCANコマンドがサーバーのリソースを消費せず、非ブロッキングである重要な理由の一つです。

まとめ

RedisのSCANコマンドは、大量のキーが保存されている環境で、Redisサーバーをブロックすることなくキー空間を安全に反復処理するための必須コマンドです。KEYSコマンドが開発・デバッグ用途に限定される一方、SCANはプロダクション環境でのキー列挙、クリーンアップ、バッチ処理、監視などに幅広く活用できます。

SCANはカーソルベースの非ブロッキングな仕組みを採用しており、MATCHCOUNTTYPEといったオプションで挙動を調整できます。しかし、スキャン中のキーの増減に対する結果のあいまいさ、CPUやネットワーク帯域への負荷、アトミック性がないことなど、いくつかの注意点も存在します。

本記事で解説したSCANコマンドの基本的な使い方、カーソルベースの仕組み、各オプションの詳細、そして重要な注意点を理解し、実際のデータ量やサーバー負荷を考慮して適切なCOUNT値の選択や処理の設計を行うことで、Redisをより安全かつ効率的に運用することができます。

また、Set, Hash, Sorted Setの要素を反復処理するためのSSCAN, HSCAN, ZSCANコマンドも、それぞれのデータ構造に対してSCANと同様のメリットを提供するため、合わせて活用を検討してください。

Redisを大規模に利用するシステムにおいて、SCANコマンドを正しく理解し、使いこなすことは、安定したサービス提供のために欠かせないスキルと言えるでしょう。


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール