はい、承知いたしました。RedisのSCAN
コマンドに関する詳細な解説記事を、約5000語のボリュームで作成し、直接ここに表示します。
Redis SCANコマンド完全マスターガイド:大規模データセットを安全かつ効率的に探索する
はじめに:なぜSCAN
コマンドが必要なのか?KEYS
コマンドの危険性
Redisは高速なインメモリデータストアとして広く利用されていますが、格納されているキーの一覧を取得したい、あるいは特定のパターンにマッチするキーを探したいというニーズは少なくありません。このような目的のために、RedisにはKEYS
コマンドが存在します。しかし、KEYS
コマンドは本番環境での使用が強く推奨されません。その理由は、KEYS
コマンドがデータベース内の全てのキーをスキャンし、その間Redisサーバーを完全にブロッキングしてしまうからです。
データベースに数百万、あるいはそれ以上のキーが格納されている場合、KEYS
コマンドの実行には数秒、数十秒、場合によってはそれ以上の時間がかかります。このブロッキング時間中、Redisサーバーは他のいかなるコマンドも処理できなくなります。これは、アプリケーションの応答遅延を引き起こしたり、最悪の場合タイムアウトによるサービス停止につながる可能性があります。特に、レイテンシが厳しく求められるリアルタイムアプリケーションにおいては、KEYS
コマンドの使用は致命的な問題となりえます。
このKEYS
コマンドの抱える問題を解決するために導入されたのが、SCAN
コマンドです。SCAN
コマンドは、データベース全体を一度にスキャンするのではなく、カーソルベースのイテレーション(反復処理)を通じて、少しずつキーを返します。これにより、Redisサーバーのブロッキング時間を最小限に抑え、オンライン中に安全にデータベースの内容を探索することが可能になります。
本記事では、このSCAN
コマンドとその関連コマンド(SSCAN
, HSCAN
, ZSCAN
)について、基本的な使い方から詳細なオプション、内部動作、使用上の注意点、ベストプラクティス、そして実践的な利用シナリオまで、徹底的に解説します。この記事を読めば、あなたはSCAN
コマンドを完全にマスターし、Redisの大規模なデータセットを安全かつ効率的に扱うことができるようになるでしょう。
SCAN
コマンドの基本
SCAN
コマンドは、キー空間(データベース全体)をイテレーションするためのコマンドです。その基本的な動作は、前回のコマンド実行時に返されたカーソルを次のコマンドに渡し、次のイテレーションを開始するというものです。このプロセスを、カーソルが0
になるまで繰り返します。
コマンド構文
redis
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
cursor
: 前回のSCAN
コマンドで返されたカーソルを指定します。最初のイテレーションでは0
を指定します。MATCH pattern
: オプション。指定されたパターンにマッチするキーのみを返します。パターンはグロブスタイル(glob-style)で、*
、?
、[]
などのワイルドカードが使用できます。COUNT count
: オプション。一度のイテレーションでRedisがスキャンしようと試みる要素数(キーの数)のヒントを指定します。実際に返される要素数はこれと異なる場合があります。デフォルト値は10です。TYPE type
: オプション。指定されたデータ型を持つキーのみを返します。指定可能な型はstring
,list
,set
,zset
,hash
,stream
,module
です。
戻り値
SCAN
コマンドは、常に2つの要素を持つ配列を返します。
new_cursor
: 次のイテレーションで使用すべき新しいカーソル。イテレーションが完了した場合は0
が返されます。elements
: 現在のイテレーションでスキャンされた要素(キー名)のリスト。
127.0.0.1:6379> SCAN 0
1) "17"
2) 1) "key:123"
2) "another:key"
3) "mykey"
... (キーのリスト)
上記の例では、SCAN 0
を実行した結果、次のカーソルとして17
が返され、いくつかのキー名がリストとして返されています。次のイテレーションでは、SCAN 17
のように、返されたカーソル17
を指定してコマンドを実行します。
カーソルの仕組みとイテレーションの流れ
SCAN
コマンドは、内部的にRedisのハッシュテーブルのバケットを走査しています。データベース全体のキーは、内部的には複数のバケットに分散して格納されています(辞書としても知られる構造)。SCAN
のカーソルは、このハッシュテーブルの走査位置を示しています。
イテレーションの基本的な流れは以下のようになります。
- 最初の
SCAN
コマンドをカーソル0
で実行します。 - Redisは、現在のカーソル位置から指定された
COUNT
のヒントに基づいて、ハッシュテーブルのバケットをいくつかスキャンします。 - スキャンしたバケットから見つかったキーのうち、
MATCH
やTYPE
の条件を満たすものを抽出します。 - 抽出したキーのリストと、次のイテレーションで使用するための新しいカーソルをクライアントに返します。
- クライアントは、返された新しいカーソルを使用して、次の
SCAN
コマンドを実行します。 - このプロセスを、返されたカーソルが
0
になるまで繰り返します。カーソルが0
になった時点で、データベース全体の走査が完了したとみなされます。
重要な点: SCAN
はステートレスなコマンドです。サーバー側でクライアントごとの状態を保持しません。各リクエストで渡されるカーソルが、スキャン処理の継続を可能にしています。これにより、サーバーのリソース消費を抑えつつ、複数のクライアントが同時にスキャンを実行することも可能になります。
イテレーション完了の判断
イテレーションが完了したかどうかは、SCAN
コマンドが返したカーソルが0
になったかどうかで判断します。最初のコマンドをカーソル0
で開始し、返されたカーソルが再び0
になるまでコマンドを繰り返し実行します。
“`python
PythonでのSCANイテレーション例
import redis
r = redis.Redis(…)
cursor = 0
all_keys = []
while True:
# SCANコマンドを実行
# match=’mykey:‘, count=100 などのオプションを追加可能
response = r.scan(cursor=cursor, match=’‘, count=100)
# 戻り値のパース: [新しいカーソル(bytes), キーのリスト(bytes)]
cursor = int(response[0])
keys = [key.decode('utf-8') for key in response[1]]
all_keys.extend(keys)
# カーソルが0になったら終了
if cursor == 0:
break
print(f”Found {len(all_keys)} keys.”)
“`
このループ構造は、SCAN
ファミリーコマンド(SSCAN
, HSCAN
, ZSCAN
)すべてに共通する基本的なイテレーションパターンです。
SCAN
コマンドのオプション詳細
SCAN
コマンドは、オプションを組み合わせることでより柔軟な探索が可能です。
MATCH pattern
MATCH
オプションを使用すると、指定されたパターンにマッチするキー名のみを結果として返します。パターンはRedisのKEYS
コマンドと同じグロブスタイルで指定します。
*
: 任意の文字列(空文字列を含む)にマッチします。例:user:*
はuser:1
,user:profile
などにマッチします。?
: 任意の一文字にマッチします。例:user:??
はuser:01
,user:ab
などにマッチしますが、user:1
やuser:abc
にはマッチしません。[...]
: ブラケット内の任意の1文字にマッチします。範囲指定も可能です(例:[0-9]
)。例:user:[0-9]
はuser:0
からuser:9
にマッチします。[^...]
: ブラケット内の文字以外の任意の1文字にマッチします。
注意点:
MATCH
オプションは、スキャンされた全てのキーに対してパターンマッチングを実行します。これは、COUNT
オプションで指定した数だけ物理的にキーをスキャンし、その中からパターンに合うものを抽出するという動作になります。したがって、MATCH
パターンにマッチするキーが非常に少ない場合でも、多くのキーがスキャンされることになり、特定のパターンにマッチするキーだけを効率的に取得できるわけではありません。全キーをスキャンする過程で、パターンに合うものがあれば返す、という動作に近いイメージです。MATCH
オプションは、SCAN
コマンドの性能に影響を与える可能性があります。特に複雑なパターンや、大量のキーがパターンにマッチする場合、サーバーのCPU使用率が上昇する可能性があります。MATCH
オプションを使用しても、返されるキーのリストが即座にパターンにマッチするキーだけになるわけではありません。COUNT
で指定した数だけキーを走査し、その中からマッチしたものを返します。したがって、一度の応答で返されるキーが全くない場合もあります。
COUNT count
COUNT
オプションは、一度のイテレーションでRedisが内部的にスキャンするハッシュテーブルのバケット数や、そこから取り出す要素数に関するヒントです。デフォルト値は10です。
COUNT
は最小保証ではありません。例えばCOUNT 1000
と指定しても、実際に返されるキーが1000個になるとは限りません。返されるキーの数は、そのイテレーションでスキャンされたバケットに格納されているキーの数や、MATCH
/TYPE
オプションによるフィルタリング結果に依存します。返されるキーは0個の場合もあります。COUNT
はRedisのハッシュテーブルの走査効率に影響を与えます。COUNT
を大きくする:- 一度のラウンドトリップ(クライアント-サーバー間の通信)でより多くのキーを取得できる可能性が高まります。これにより、全キーをスキャンするために必要なラウンドトリップの回数が減り、クライアント側の処理にかかる時間が短縮される可能性があります。
- Redisサーバーが一度に多くのバケットやキーを処理するため、一時的なCPU負荷が高まる可能性があります。ネットワーク帯域もより多く消費します。
COUNT
を小さくする:- サーバーの一時的な負荷は抑えられます。
- 全キーをスキャンするために必要なラウンドトリップの回数が増加します。クライアント側の処理時間(特にネットワークレイテンシが高い場合)が増加する可能性があります。
適切なCOUNT
の値は、ネットワーク環境、Redisサーバーの負荷状況、データベースのキー数や分布など、様々な要因に依存します。一般的には、ある程度大きな値を指定することで、ラウンドトリップ数を減らし全体の効率を上げることが推奨されますが、サーバーの応答が遅延したり、CPU使用率が急増したりしないか、監視しながら調整することが重要です。COUNT
はあくまでヒントであるため、指定した値と実際にスキャンされる数が大きく異なる場合があることを理解しておきましょう。特に、Redisのハッシュテーブルがリサイズされる過程では、COUNT
のヒントから大きく外れた数のキーが処理されることがあります。
TYPE type
TYPE
オプションを使用すると、指定されたデータ型を持つキーのみをスキャンし、結果として返します。
指定可能な型は以下の通りです。
* string
* list
* set
* zset
(sorted set)
* hash
* stream
* module
(カスタムモジュールが定義した型)
例:
redis
SCAN 0 TYPE hash
これは、データベース内のハッシュ型のキーのみをスキャンします。
利点:
- 特定の型のキーだけが必要な場合に非常に効率的です。不要な型のキーをフィルタリングする手間が省けます。
MATCH
オプションと組み合わせて、特定のパターンを持ち、かつ特定の型であるキーを検索できます。
redis
SCAN 0 MATCH user:* TYPE hash
これは、パターンuser:*
にマッチし、かつハッシュ型であるキーのみをスキャンします。
注意点:
TYPE
オプションも、スキャンされた全てのキーに対して型チェックを実行します。これは、MATCH
オプションと同様に、COUNT
のヒントに基づいて内部的にスキャンされたキーの中から、指定された型に合致するものを抽出するという動作になります。TYPE
オプションを使用した場合も、一度の応答で返されるキーが0個になる可能性はあります。
TYPE
オプションは、特に大規模なデータセットで特定の型のキーを対象に操作を行いたい場合に非常に役立ちます。例えば、全てのセッションデータを(ハッシュ型で格納しているとして)取得したい場合などに有効です。
SCAN
ファミリーコマンド:Set, Hash, Sorted Setの要素をスキャンする
SCAN
コマンドはデータベースのキー空間をスキャンしますが、Redisの複合データ型(Set, Hash, Sorted Set)の内部要素についても、同様にカーソルベースでイテレーションを行うための専用コマンドが用意されています。これらはSCAN
ファミリーコマンドと呼ばれます。
SSCAN
:Setの要素をスキャンする
SSCAN key cursor [MATCH pattern] [COUNT count]
指定されたSet型のキーのメンバー(要素)をイテレーションします。
* key
: スキャン対象のSet型のキー名。
* cursor
: 前回のSSCAN
で返されたカーソル(初回は0
)。
* オプション: MATCH pattern
, COUNT count
はSCAN
コマンドと同様です。ただし、MATCH
はSetのメンバーに対してパターンマッチングを行います。
戻り値は2つの要素を持つ配列です。
1. new_cursor
: 次のイテレーションで使用すべき新しいカーソル。完了時は0
。
2. members
: 現在のイテレーションでスキャンされたSetのメンバーのリスト。
例:
“`redis
Set ‘myset’ に要素を追加
SADD myset a b c d e f g h i j k l m n o p q r s t u v w x y z
SSCANで要素をイテレーション (COUNT 5のヒント付き)
127.0.0.1:6379> SSCAN myset 0 COUNT 5
1) “14” # 新しいカーソル
2) 1) “g” # 要素リスト
2) “p”
3) “l”
4) “a”
5) “k”
“`
Setは順序を保持しないため、返される要素の順番は不定です。
HSCAN
:Hashのフィールドと値をスキャンする
HSCAN key cursor [MATCH pattern] [COUNT count]
指定されたHash型のキーのフィールドと値のペアをイテレーションします。
* key
: スキャン対象のHash型のキー名。
* cursor
: 前回のHSCAN
で返されたカーソル(初回は0
)。
* オプション: MATCH pattern
, COUNT count
はSCAN
コマンドと同様です。ただし、MATCH
はHashのフィールド名に対してパターンマッチングを行います。
戻り値は2つの要素を持つ配列です。
1. new_cursor
: 次のイテレーションで使用すべき新しいカーソル。完了時は0
。
2. field_value_pairs
: 現在のイテレーションでスキャンされたフィールドと値のペアのリスト。フィールド名と値が交互に格納された配列として返されます。
例:
“`redis
Hash ‘myhash’ にフィールドと値を追加
HMSET myhash field1 value1 field2 value2 field3 value3 field4 value4
HSCANでフィールド/値ペアをイテレーション
127.0.0.1:6379> HSCAN myhash 0
1) “0” # 新しいカーソル (要素が少ないため1回のイテレーションで完了)
2) 1) “field1” # 要素リスト (フィールド名と値が交互)
2) “value1”
3) “field2”
4) “value2”
5) “field3”
6) “value3”
7) “field4”
8) “value4”
“`
Hashも順序を保持しないため、返されるフィールド/値ペアの順番は不定です。
ZSCAN
:Sorted Setのメンバーとスコアをスキャンする
ZSCAN key cursor [MATCH pattern] [COUNT count]
指定されたSorted Set型のキーのメンバーとスコアのペアをイテレーションします。
* key
: スキャン対象のSorted Set型のキー名。
* cursor
: 前回のZSCAN
で返されたカーソル(初回は0
)。
* オプション: MATCH pattern
, COUNT count
はSCAN
コマンドと同様です。ただし、MATCH
はSorted Setのメンバーに対してパターンマッチングを行います。
戻り値は2つの要素を持つ配列です。
1. new_cursor
: 次のイテレーションで使用すべき新しいカーソル。完了時は0
。
2. member_score_pairs
: 現在のイテレーションでスキャンされたメンバーとスコアのペアのリスト。メンバーとスコアが交互に格納された配列として返されます。スコアは文字列として返されます。
例:
“`redis
Sorted Set ‘myzset’ にメンバーとスコアを追加
ZADD myzset 10 “member:a” 20 “member:b” 30 “member:c” 40 “member:d”
ZSCANでメンバー/スコアペアをイテレーション (MATCH member:*)
127.0.0.1:6379> ZSCAN myzset 0 MATCH member:*
1) “0” # 新しいカーソル (要素が少ないため1回のイテレーションで完了)
2) 1) “member:a” # 要素リスト (メンバーとスコアが交互)
2) “10”
3) “member:b”
4) “20”
5) “member:c”
6) “30”
7) “member:d”
8) “40”
``
ZSCAN`のイテレーション順序はスコア順ではありません。内部的な構造の走査順序に従います。
Sorted Setはスコアによる順序を保持しますが、
SCAN
ファミリーコマンドは、Redisに格納されている複合データ型の内部構造を、SCAN
と同様のノンブロッキングな方法で探索するために非常に重要です。これらのコマンドを使用することで、Set, Hash, Sorted Setに含まれる大量の要素に対しても、サーバーに大きな負荷をかけることなく処理を行うことができます。
SCAN
の動作原理と内部実装(簡易版)
SCAN
コマンドがどのようにノンブロッキングなイテレーションを実現しているのかを理解するためには、Redisがキーや複合データ型の要素をどのように内部で管理しているかを知る必要があります。
Redisのメインデータベース(キー空間)は、ハッシュテーブル(正確には、ディクショナリ構造)で実装されています。このハッシュテーブルは、複数の「バケット」と呼ばれるスロットの配列で構成されており、各バケットにはキーとその値(または値へのポインタ)が格納されています。
SCAN
コマンドのカーソルは、このハッシュテーブルのバケット配列内のインデックスのようなものだと考えることができます(ただし、実際のカーソル値とバケットインデックスは直接対応するわけではありません)。SCAN
は、指定されたカーソル位置から開始し、内部的にいくつかのバケットを順番に走査します。COUNT
オプションは、この走査するバケット数やそこから要素を取り出す量のヒントとなります。
ハッシュテーブルのリサイズとSCAN
の挙動
Redisのハッシュテーブルは、格納されるキーの数が増減するにつれて、効率を維持するために動的にサイズが変更(リサイズ)されます。リサイズは通常、バックグラウンドで段階的に行われます(”incremental rehashing”)。新しいハッシュテーブルが作成され、古いハッシュテーブルのバケットから新しいテーブルへ少しずつキーが移動されます。
SCAN
コマンドは、このリサイズ中のハッシュテーブルに対しても動作します。このとき、SCAN
は古いテーブルと新しいテーブルの両方を走査します。このリサイズ処理と並行してSCAN
が実行されることにより、いくつかの注意点が発生します。
- 要素の重複: リサイズ中に、まだ古いテーブルに残っている要素と、新しいテーブルに既に移動した同じ要素の両方がスキャンされてしまう可能性があります。
- 要素のスキップ: ごくまれに、リサイズのごく特定のタイミングで要素がスキップされる可能性もゼロではありません。
これらの特性のため、SCAN
コマンドによるイテレーションはスナップショットではありません。イテレーション中にデータが変更(追加、削除、更新)された場合、それらの変更がイテレーション結果に反映されたり、前述の重複やスキップが発生したりする可能性があります。
アトミックではない性質
SCAN
コマンド全体(カーソル0
から開始して0
に戻るまでの一連のイテレーション)はアトミックではありません。各SCAN
コマンドの呼び出し自体はアトミックですが、一連のイテレーション全体として見ると、その途中で他のクライアントがデータの読み書きを行う可能性があります。
このアトミックではない性質と、イテレーション中のデータ変更、そしてハッシュテーブルのリサイズ処理によって、SCAN
コマンドは以下の特性を持ちます。
- 全要素の網羅保証なし: 基本的には全ての要素をスキャンするように設計されていますが、リサイズ中のデータ変更によっては、ごくまれに要素がスキップされる可能性があります。
- 要素の重複可能性あり: イテレーション中に要素が移動(リサイズ)または追加された場合、重複して取得される可能性があります。
- イテレーション順序は不定: ハッシュテーブルの内部的な構造によって決定されるため、要素が追加された順やキー名/スコア順など、特定の論理的な順序で返されるわけではありません。
これらの特性は、SCAN
を使用する上で非常に重要です。もし「絶対に重複なく、漏れなく、スキャン開始時点での正確なスナップショットを取得したい」という要件がある場合は、SCAN
は適していません(そもそもRedis自体がそのようなスナップショット取得を目的としたデータベースではありません)。SCAN
はあくまで、「サーバーに負荷をかけずに、データベース全体(あるいは複合データ型の内部)を探索するための手段」と理解するべきです。重複や取得漏れが許容できない処理を行う場合は、スキャン結果を後処理でフィルタリングしたり、データベースを一時停止したり(ただしこれはKEYS
と同様の問題を引き起こす可能性がある)などの追加の考慮が必要です。
SCAN
を使う上での注意点とベストプラクティス
SCAN
コマンドはRedisの運用において非常に有用ですが、その特性を理解せずに使用すると予期せぬ問題を引き起こす可能性があります。ここでは、SCAN
を使う上での注意点とベストプラクティスを解説します。
イテレーションの完了保証:カーソルが0になるまで繰り返す
最も基本的ながら重要な点です。データベース全体(または複合データ型の全要素)をスキャンするには、必ずカーソル0
から開始し、コマンドの戻り値として返されたカーソルが再び0
になるまで、イテレーションを繰り返す必要があります。途中でイテレーションを中断した場合、全てをスキャンしたことにはなりません。
データ変更中のスキャン:重複とスキップの可能性を理解する
前述のように、SCAN
のイテレーション中にデータベースのデータが追加、変更、削除されると、結果に重複やスキップが発生する可能性があります。
- 追加された要素: イテレーションの途中、あるいはこれからスキャンされるバケットに新しい要素が追加された場合、それはスキャン結果に含まれる可能性があります。
- 削除された要素: イテレーションの途中、あるいは既にスキャンされたバケットから要素が削除された場合、それはスキャン結果に含まれなくなります(既に取得済みの場合は手元には残ります)。これからスキャンされるバケットから要素が削除された場合、当然スキャン結果には含まれません。
- 変更された要素: キーの値などが変更されても、通常は
SCAN
の結果(キー名や複合型のメンバー/フィールド名)には直接影響しません。しかし、複合型でキーやメンバー自体が削除・追加されたり、ハッシュテーブルのリサイズが発生したりする場合は、前述の重複・スキップの可能性があります。
これらの挙動は、SCAN
がデータベースのスナップショットを取得するのではなく、あくまで現在のデータベースの状態を「ゆるやかに」走査していることに起因します。もし、取得したキー/要素に対して後続処理を行う場合、その処理が重複に対応できるか、あるいは存在しない要素に対する操作を安全に行えるか(例: GET key
が nil
を返しても大丈夫か)などを考慮する必要があります。
性能への影響:サーバー負荷とCOUNT
の調整
SCAN
コマンドはKEYS
コマンドのようなブロッキングは起こしませんが、全く負荷がかからないわけではありません。
- CPU使用率:
SCAN
は、スキャンするバケットや要素数に応じてCPUを使用します。特にCOUNT
を大きくしたり、MATCH
パターンが複雑だったり、対象となるキー/要素が多い場合は、一時的にCPU使用率が上昇する可能性があります。 - ネットワーク帯域: 一度に多くの要素を返す設定(大きな
COUNT
)にすると、ネットワーク帯域の消費量が増加します。
これらの性能影響を最小限に抑えるためには、以下の点を考慮してください。
- 適切な
COUNT
値の選定: 環境や要件に応じてCOUNT
値を調整します。まずはデフォルト値(10)や少し大きめの値(100〜1000程度)から試してみて、サーバーのCPU負荷やネットワーク使用量、クライアント側のスキャン完了までの時間などを監視しながら最適な値を見つけます。大量のキーを持つデータベースでは、COUNT
を数百〜数千程度に設定することが一般的です。 MATCH
オプションの利用: 特定のパターンにマッチするキーだけが必要な場合はMATCH
を利用しますが、これがサーバー側でフィルタリングを行うため、その処理コストが発生します。TYPE
オプションの活用: 特定の型のキーだけが必要な場合は、必ずTYPE
オプションを指定します。これにより、不要な型のキーをクライアント側でフィルタリングする手間が省け、ネットワーク帯域の節約にもつながります。- 実行頻度と実行タイミング:
SCAN
処理は、サーバーのピーク時間帯を避けて実行したり、スキャン処理自体を低優先度のバックグラウンドジョブとして実行したりするなど、運用上の考慮が必要です。
クライアント側の実装:カーソル管理とエラー処理
SCAN
を安全かつ確実に実行するためには、クライアント側で以下の点を適切に実装する必要があります。
- カーソル管理: 前回の
SCAN
コマンドで返されたカーソル値を正確に保持し、次のコマンドに渡す必要があります。カーソルは数値(文字列として返される)ですが、多くのクライアントライブラリはこれを自動的に処理してくれます。 - ループ処理: カーソルが
0
になるまで、SCAN
コマンドを繰り返し実行するループを適切に実装します。 - エラー処理: ネットワークエラーやRedisサーバー側のエラー(例:
OOM command not allowed when used memory > 'maxmemory'
)が発生した場合の処理を実装します。エラーが発生した場合、イテレーションを中断し、適切なログ出力や再試行処理などを行う必要があります。ただし、エラーから回復してイテレーションを再開する場合、中断したカーソル値から再開することで、既にスキャンした部分をスキップできる可能性があります(完全に保証されるわけではありません)。 - 取得した要素のバッファリング/処理: 各イテレーションで返される要素は一時的なリストです。これらの要素を全て集めて処理したい場合は、クライアント側でリストに追記したり、ファイルに書き出したりするなどのバッファリングや逐次処理が必要です。大量の要素をメモリに保持すると、クライアント側のメモリを圧迫する可能性があるため注意が必要です。
多くのRedisクライアントライブラリは、SCAN
イテレーションを容易にするためのヘルパー関数やイテレーターを提供しています。これらを活用することで、カーソル管理やループ処理の実装を簡略化できます。
大規模データセットでの運用
非常に大規模なデータセット(数億以上のキー)に対してSCAN
を実行する場合、単にループを回すだけでなく、さらに高度な考慮が必要になることがあります。
- 進捗の保存: スキャン処理に非常に時間がかかる場合、途中で処理が中断されたときに、どこまでスキャンが進んだかを保存しておくことが望ましいです。これにより、中断箇所から再開し、最初からやり直すのを避けることができます。返されたカーソル値を外部ストレージ(ファイル、別のRedisインスタンスなど)に保存しておけば、再開時にそのカーソル値を指定して
SCAN
を実行できます。 - 分散処理: スキャン処理自体を複数のワーカーに分散させることも理論的には可能ですが、これはRedisの内部的なハッシュテーブル構造とカーソルシステムを深く理解している必要があり、実装は複雑になります。通常は単一クライアントからのイテレーションで十分です。
- レプリカでの実行: 読み取り操作である
SCAN
は、マスターサーバーではなくレプリカサーバーで実行することで、マスターサーバーへの負荷を軽減できます。ただし、レプリケーション遅延がある場合、レプリカ上のデータはマスターの最新状態を反映していない可能性があるため、スキャン結果もマスターの瞬間の状態とは異なる可能性があることに留意が必要です。
SCAN
とKEYS
の比較:なぜSCAN
が推奨されるのか
改めて、SCAN
とKEYS
コマンドの比較を通じて、なぜSCAN
が推奨されるのかを明確にします。
特徴 | KEYS pattern |
SCAN cursor [options] |
---|---|---|
ブロッキング | 完全にブロッキング(データベースサイズに比例) | ノンブロッキング(一度のスキャン量はCOUNT や内部構造に依存) |
性能影響 | 大規模データセットでは致命的な遅延を引き起こす | COUNT に応じたCPU使用、ネットワーク帯域消費。調整可能。 |
アトミック性 | 単一コマンドなのでアトミック | イテレーション全体としてはアトミックではない |
スナップショット性 | 実行時点のスナップショット(ただしブロッキング) | スナップショットではない(イテレーション中の変更を反映) |
全要素網羅 | 保証される | 基本的に網羅するが、データ変更やリサイズ中に重複/スキップの可能性あり |
返される要素数 | マッチする全要素を一度に返す | 一度の応答で返される要素数は不定 |
メモリ使用 | サーバー側で全結果をバッファするため大量のメモリを消費する可能性あり | 各イテレーションの小さな結果セットのみを返すため、サーバーメモリ消費は限定的 |
用途 | 開発/テスト環境、ごく小規模なデータセットのみ | 本番環境での安全な探索、メンテナンス、棚卸し |
KEYS
コマンドは、小規模な開発/テスト環境で一時的に全キーを確認したい場合などに限定して使用すべきです。本番環境でKEYS
コマンドを実行することは、Redisサーバーの安定性やアプリケーションの可用性に深刻な影響を与える可能性があるため、絶対に避けるべきです。
一方、SCAN
コマンドは、データベース全体の規模に関わらず、オンライン中に安全にキーや要素を探索するための標準的な方法です。データ変更による結果の不確実性(重複・スキップ)はありますが、サーバーをブロッキングしないという最大の利点が、本番環境での利用においてSCAN
を唯一の選択肢としています。
実践的な利用シナリオ
SCAN
およびSCAN
ファミリーコマンドは、様々な運用・開発シナリオで活用できます。
- 全キー/特定パターンキーの棚卸し・一覧取得: データベースにどのようなキーが存在するか、あるいは特定の命名規則に従ったキーがどれだけあるかを知りたい場合に利用します。取得したキーリストをファイルに保存したり、別のシステムで処理したりします。
- 特定の型のキーに対する一括処理: 例えば、全てのHash型のキーに対して何か処理を行いたい場合(例: 構造のチェック、特定のフィールドの集計など)、
SCAN 0 TYPE hash
でHash型のキーを効率的に取得し、それぞれのキーに対してHGETALL
やHSCAN
などで内部を処理します。 - 期限切れキーの特定(参考): Redisはキーの有効期限(TTL)を自動的に管理しますが、明示的に期限切れ間近のキーや期限切れになってしまったキーを特定したい場合に、
SCAN
でキー名を取得し、それぞれのTTL
をチェックするという方法が考えられます。ただし、これも全てのキーをチェックするためには時間がかかり、TTL情報はリアルタイムに変動するため、あくまで補助的な手段と考えた方が良いでしょう。Redisの自動的なキー削除機構に任せるのが一般的です。 - キャッシュウォーミング: アプリケーション起動時に、よく使われるデータを事前にキャッシュにロードする「キャッシュウォーミング」処理において、特定のパターンを持つキーを
SCAN
で取得し、それらのデータを読み込むという使い方が考えられます。 - データのエクスポート/移行: Redisインスタンス間でデータを移行する際や、データを外部ストレージにエクスポートする際に、
SCAN
でキーや要素を抽出し、逐次処理することが可能です。DUMP
コマンドとRESTORE
コマンドも移行には使えますが、データベース全体または特定のキーセットを細かく制御してエクスポートしたい場合にSCAN
が役立ちます。 - インデックスの再構築やメンテナンス: セカンダリインデックスなどを自前で構築している場合、元データ(Redis内のキーや要素)の変更に合わせてインデックスを更新する必要があります。
SCAN
を使用して元データを定期的に走査し、インデックスとの整合性をチェックしたり、インデックスを再構築したりする処理の起点とすることができます。 - Set, Hash, Sorted Setの要素に対する一括処理: 特定のSetに含まれる全ユーザーIDを取得して何か処理を行いたい、Hashに格納された全ユーザープロファイルデータをチェックしたい、Sorted Setの全ランキングメンバーを処理したい、といった場合に、
SSCAN
,HSCAN
,ZSCAN
が役立ちます。
SCAN
コマンドの実装例(Python)
ここでは、代表的なRedisクライアントライブラリであるredis-py
を使ったSCAN
イテレーションのPython実装例を示します。他の言語のライブラリでも、同様のイテレーションパターン(カーソル管理とループ)で実装できます。
“`python
import redis
Redisへの接続設定
実際の接続情報に置き換えてください
r = redis.StrictRedis(host=’localhost’, port=6379, db=0, decode_responses=True) # decode_responses=Trueで文字列として取得
def scan_all_keys(redis_client, match_pattern=’‘, count_hint=1000, key_type=None):
“””
Redisインスタンスから全てのキーをSCANコマンドで取得するジェネレータ
:param redis_client: redis.Redis または redis.StrictRedis インスタンス
:param match_pattern: MATCHオプションのパターン (デフォルト: ‘‘)
:param count_hint: COUNTオプションのヒント値 (デフォルト: 1000)
:param key_type: TYPEオプションの型 (例: ‘hash’, ‘set’など。Noneの場合は全型)
:return: キー名を一つずつyieldするジェネレータ
“””
cursor = 0
while True:
# SCANコマンドを実行
# key_typeが指定されていればtypeオプションを追加
scan_kwargs = {‘match’: match_pattern, ‘count’: count_hint}
if key_type:
scan_kwargs[‘type’] = key_type
response = redis_client.scan(cursor=cursor, **scan_kwargs)
# 戻り値のパース: [新しいカーソル, キーのリスト]
cursor = int(response[0])
keys = response[1] # decode_responses=True なので文字列リスト
# 取得したキーを一つずつyield
for key in keys:
yield key
# カーソルが0になったら終了
if cursor == 0:
break
def scan_hash_fields(redis_client, key, match_pattern=’*’, count_hint=100):
“””
Hash型のキーのフィールドと値のペアをHSCANコマンドで取得するジェネレータ
:param redis_client: redis.Redis または redis.StrictRedis インスタンス
:param key: HSCAN対象のHashキー名
:param match_pattern: MATCHオプションのパターン (フィールド名に対してマッチ)
:param count_hint: COUNTオプションのヒント値
:return: (フィールド名, 値) のペアを一つずつyieldするジェネレータ
“””
cursor = 0
while True:
response = redis_client.hscan(name=key, cursor=cursor, match=match_pattern, count=count_hint)
cursor = int(response[0])
items = response[1] # {field: value, ...} 形式の辞書
# 取得したフィールド/値ペアを一つずつyield
for field, value in items.items():
yield (field, value)
if cursor == 0:
break
— 使用例 —
全てのキーをスキャンして表示
print(“Scanning all keys:”)
try:
for key in scan_all_keys(r, count_hint=500):
print(f” Found key: {key}”)
except Exception as e:
print(f”An error occurred during SCAN: {e}”)
特定のパターンを持つキーをスキャンして表示
print(“\nScanning keys matching ‘user:‘:”)
try:
for key in scan_all_keys(r, match_pattern=’user:‘, count_hint=100):
print(f” Found user key: {key}”)
except Exception as e:
print(f”An error occurred during SCAN: {e}”)
Hash型のキーのみをスキャンして表示
print(“\nScanning hash type keys:”)
try:
for key in scan_all_keys(r, key_type=’hash’, count_hint=200):
print(f” Found hash key: {key}”)
# 見つかったハッシュキーのフィールドをHSCANでスキャンする例 (ネストされたスキャン)
print(f" Scanning fields of hash key: {key}")
try:
for field, value in scan_hash_fields(r, key, count_hint=50):
print(f" Field: {field}, Value: {value}")
except Exception as hs_e:
print(f" An error occurred during HSCAN for key {key}: {hs_e}")
except Exception as e:
print(f”An error occurred during SCAN for hash type: {e}”)
SSCAN, ZSCAN も同様のパターンで実装可能
redis-pyのscan_iter, hscan_iter, sscan_iter, zscan_iter メソッドを使うと、
上記のwhileループとカーソル管理を自動で行ってくれるため、より簡単に記述できます。
scan_iterを使った例
print(“\nScanning all keys using scan_iter:”)
try:
for key in r.scan_iter(match=’*’, count=500):
print(f” Found key (iter): {key}”)
except Exception as e:
print(f”An error occurred during scan_iter: {e}”)
“`
上記のコードでは、ジェネレータ関数としてscan_all_keys
とscan_hash_fields
を実装しています。これにより、スキャンされたキーやフィールドをメモリ上に全て読み込むことなく、一つずつ処理することが可能です。redis-py
のような多くのクライアントライブラリには、scan_iter
のような便利なイテレーターメソッドが組み込まれており、これを利用するとさらに簡潔にSCAN
処理を記述できます。
よくある質問 (FAQ)
Q1: COUNT
を非常に大きくすれば、SCAN
は速くなりますか?
必ずしも速くなるわけではありません。COUNT
はあくまでヒントであり、一度に多くのキーをスキャンしようと試みますが、実際に返されるキーの数は保証されません。COUNT
を大きくしすぎると、一度のリクエストでRedisサーバーのCPU負荷が急増したり、クライアントとサーバー間のネットワーク通信量が増大したりする可能性があります。結果として、サーバーの応答が遅延したり、クライアント側の処理が追いつかなくなったりして、かえって全体のスキャン完了時間が延びたり、安定性が損なわれたりすることがあります。適切なCOUNT
値は、環境(キー数、サーバー性能、ネットワーク)に応じて監視しながら調整する必要があります。
Q2: SCAN
のイテレーション中にキーを削除したり追加したりしても大丈夫ですか?
はい、SCAN
はデータ変更と並行して動作するように設計されています。ただし、前述の通り、イテレーション中に削除されたキーは結果に含まれなくなる可能性があり、追加されたキーは結果に含まれる可能性があります。また、ハッシュテーブルのリサイズとデータ変更が重なると、ごくまれに要素の重複やスキップが発生する可能性があります。SCAN
はスナップショットではないため、このような挙動は仕様の一部です。
Q3: SCAN
はアトミックですか?
いいえ、SCAN
コマンドの「一回の呼び出し」はアトミックですが、「カーソルが0に戻るまでの一連のイテレーション全体」はアトミックではありません。各SCAN
コマンド呼び出しの間で、他のクライアントからのコマンドが実行される可能性があります。
Q4: SCAN
で取得漏れや重複はありますか?
はい、Redisのハッシュテーブルのリサイズ処理とデータ変更が並行して行われる場合、ごくまれに取得漏れ(スキップ)が発生する可能性が理論上あります。より頻繁に発生しうるのは重複です。これはSCAN
の設計上の特性であり、全要素を完全に網羅し、かつ重複がないことを絶対的に保証するものではありません(ただし、通常の使用においては多くの要素を網羅できます)。もし厳密なスナップショットが必要な場合は、他の方法(例えば、RDBファイルを解析するなど)を検討する必要があります。
Q5: SCAN
はマスターで実行すべきですか、レプリカで実行すべきですか?
SCAN
は読み取り操作なので、マスターサーバーへの負荷を分散するためにレプリカサーバーで実行することが推奨されます。ただし、レプリケーション遅延がある場合、レプリカ上のデータはマスターの最新状態ではない可能性があるため、その点は考慮が必要です。ほとんどのユースケースでは、多少の遅延を許容してレプリカで実行する方が、マスターの可用性を維持する上で優れています。
Q6: MATCH
パターンで大文字・小文字を区別しますか?
Redisのキー名自体はバイナリセーフであり、大文字・小文字を含めることができます。MATCH
パターンでのマッチングも、デフォルトでは大文字・小文字を区別します。もし区別したくない場合は、キー名を格納する際に全て小文字にするなどの工夫が必要です。
まとめ
RedisのSCAN
コマンドは、KEYS
コマンドが持つブロッキングの問題を解決し、大規模なデータセットに対しても安全かつ効率的にキーや要素を探索するための不可欠なツールです。カーソルベースのイテレーション、ノンブロッキングな動作、MATCH
、COUNT
、TYPE
といった柔軟なオプションにより、様々なシナリオに対応できます。
SCAN
ファミリーコマンドであるSSCAN
、HSCAN
、ZSCAN
も同様の原則に基づいて、Set、Hash、Sorted Setといった複合データ型の内部要素を安全に走査する方法を提供します。
SCAN
を使用する上では、その特性、特にイテレーション中に発生しうるデータ変更による重複やスキップの可能性、そしてイテレーション全体がアトミックではない性質を理解することが非常に重要です。また、適切なCOUNT
値の選定や、クライアント側での堅牢なカーソル管理とエラー処理の実装が、大規模データセットでの安定運用には欠かせません。
本番環境において、Redisの全キーや特定の要素を探索する必要が生じた場合は、迷わずSCAN
ファミリーコマンドを選択してください。KEYS
コマンドは開発・テスト環境以外では使用しないというルールを徹底することが、Redisサーバーの健全性を維持し、アプリケーションの可用性を確保するための鍵となります。
この記事が、あなたのRedis運用におけるSCAN
コマンドの理解と活用の一助となれば幸いです。
参考資料
- Redis Documentation: SCAN command: https://redis.io/commands/scan/
- Redis Documentation: SSCAN command: https://redis.io/commands/sscan/
- Redis Documentation: HSCAN command: https://redis.io/commands/hscan/
- Redis Documentation: ZSCAN command: https://redis.io/commands/zscan/
- Redis Documentation: How SCAN works: https://redis.io/docs/latest/commands/scan/scan-how-it-works/
- Redis Documentation: Incremental rehashing: https://redis.io/docs/latest/internal/dict/#incremental-rehashing
上記でRedisのSCAN
コマンドに関する詳細な解説記事を記述しました。文字数は約5000語を目標に、基本的な使い方から内部動作、注意点、ベストプラクティス、実践例、FAQまで網羅しました。
はい、承知いたしました。RedisのSCAN
コマンドに関する詳細な解説記事を、約5000語のボリュームで作成し、直接ここに表示します。
Redis SCANコマンド完全マスターガイド:大規模データセットを安全かつ効率的に探索する
はじめに:なぜSCAN
コマンドが必要なのか?KEYS
コマンドの危険性
Redisは高速なインメモリデータストアとして広く利用されていますが、格納されているキーの一覧を取得したい、あるいは特定のパターンにマッチするキーを探したいというニーズは少なくありません。このような目的のために、RedisにはKEYS
コマンドが存在します。しかし、KEYS
コマンドは本番環境での使用が強く推奨されません。その理由は、KEYS
コマンドがデータベース内の全てのキーをスキャンし、その間Redisサーバーを完全にブロッキングしてしまうからです。
データベースに数百万、あるいはそれ以上のキーが格納されている場合、KEYS
コマンドの実行には数秒、数十秒、場合によってはそれ以上の時間がかかります。このブロッキング時間中、Redisサーバーは他のいかなるコマンドも処理できなくなります。これは、アプリケーションの応答遅延を引き起こしたり、最悪の場合タイムアウトによるサービス停止につながる可能性があります。特に、レイテンシが厳しく求められるリアルタイムアプリケーションにおいては、KEYS
コマンドの使用は致命的な問題となりえます。
このKEYS
コマンドの抱える問題を解決するために導入されたのが、SCAN
コマンドです。SCAN
コマンドは、データベース全体を一度にスキャンするのではなく、カーソルベースのイテレーション(反復処理)を通じて、少しずつキーを返します。これにより、Redisサーバーのブロッキング時間を最小限に抑え、オンライン中に安全にデータベースの内容を探索することが可能になります。
本記事では、このSCAN
コマンドとその関連コマンド(SSCAN
, HSCAN
, ZSCAN
)について、基本的な使い方から詳細なオプション、内部動作、使用上の注意点、ベストプラクティス、そして実践的な利用シナリオまで、徹底的に解説します。この記事を読めば、あなたはSCAN
コマンドを完全にマスターし、Redisの大規模なデータセットを安全かつ効率的に扱うことができるようになるでしょう。
SCAN
コマンドの基本
SCAN
コマンドは、キー空間(データベース全体)をイテレーションするためのコマンドです。その基本的な動作は、前回のコマンド実行時に返されたカーソルを次のコマンドに渡し、次のイテレーションを開始するというものです。このプロセスを、カーソルが0
になるまで繰り返します。
コマンド構文
redis
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
cursor
: 前回のSCAN
コマンドで返されたカーソルを指定します。最初のイテレーションでは0
を指定します。MATCH pattern
: オプション。指定されたパターンにマッチするキーのみを返します。パターンはグロブスタイル(glob-style)で、*
、?
、[]
などのワイルドカードが使用できます。COUNT count
: オプション。一度のイテレーションでRedisがスキャンしようと試みる要素数(キーの数)のヒントを指定します。実際に返される要素数はこれと異なる場合があります。デフォルト値は10です。TYPE type
: オプション。指定されたデータ型を持つキーのみを返します。指定可能な型はstring
,list
,set
,zset
,hash
,stream
,module
です。
戻り値
SCAN
コマンドは、常に2つの要素を持つ配列を返します。
new_cursor
: 次のイテレーションで使用すべき新しいカーソル。イテレーションが完了した場合は0
が返されます。elements
: 現在のイテレーションでスキャンされた要素(キー名)のリスト。
127.0.0.1:6379> SCAN 0
1) "17"
2) 1) "key:123"
2) "another:key"
3) "mykey"
... (キーのリスト)
上記の例では、SCAN 0
を実行した結果、次のカーソルとして17
が返され、いくつかのキー名がリストとして返されています。次のイテレーションでは、SCAN 17
のように、返されたカーソル17
を指定してコマンドを実行します。
カーソルの仕組みとイテレーションの流れ
SCAN
コマンドは、内部的にRedisのハッシュテーブルのバケットを走査しています。データベース全体のキーは、内部的には複数のバケットに分散して格納されています(辞書としても知られる構造)。SCAN
のカーソルは、このハッシュテーブルの走査位置を示しています。
イテレーションの基本的な流れは以下のようになります。
- 最初の
SCAN
コマンドをカーソル0
で実行します。 - Redisは、現在のカーソル位置から指定された
COUNT
のヒントに基づいて、ハッシュテーブルのバケットをいくつかスキャンします。 - スキャンしたバケットから見つかったキーのうち、
MATCH
やTYPE
の条件を満たすものを抽出します。 - 抽出したキーのリストと、次のイテレーションで使用するための新しいカーソルをクライアントに返します。
- クライアントは、返された新しいカーソルを使用して、次の
SCAN
コマンドを実行します。 - このプロセスを、返されたカーソルが
0
になるまで繰り返します。カーソルが0
になった時点で、データベース全体の走査が完了したとみなされます。
重要な点: SCAN
はステートレスなコマンドです。サーバー側でクライアントごとの状態を保持しません。各リクエストで渡されるカーソルが、スキャン処理の継続を可能にしています。これにより、サーバーのリソース消費を抑えつつ、複数のクライアントが同時にスキャンを実行することも可能になります。
イテレーション完了の判断
イテレーションが完了したかどうかは、SCAN
コマンドが返したカーソルが0
になったかどうかで判断します。最初のコマンドをカーソル0
で開始し、返されたカーソルが再び0
になるまでコマンドを繰り返し実行します。
“`python
PythonでのSCANイテレーション例
import redis
r = redis.Redis(…)
cursor = 0
all_keys = []
while True:
# SCANコマンドを実行
# match=’mykey:‘, count=100 などのオプションを追加可能
response = r.scan(cursor=cursor, match=’‘, count=100)
# 戻り値のパース: [新しいカーソル(bytes), キーのリスト(bytes)]
cursor = int(response[0])
keys = [key.decode('utf-8') for key in response[1]]
all_keys.extend(keys)
# カーソルが0になったら終了
if cursor == 0:
break
print(f”Found {len(all_keys)} keys.”)
“`
このループ構造は、SCAN
ファミリーコマンド (SSCAN
, HSCAN
, ZSCAN
) すべてに共通する基本的なイテレーションパターンです。
SCAN
コマンドのオプション詳細
SCAN
コマンドは、オプションを組み合わせることでより柔軟な探索が可能です。
MATCH pattern
MATCH
オプションを使用すると、指定されたパターンにマッチするキー名のみを結果として返します。パターンはRedisのKEYS
コマンドと同じグロブスタイルで指定します。
*
: 任意の文字列(空文字列を含む)にマッチします。例:user:*
はuser:1
,user:profile
などにマッチします。?
: 任意の一文字にマッチします。例:user:??
はuser:01
,user:ab
などにマッチしますが、user:1
やuser:abc
にはマッチしません。[...]
: ブラケット内の任意の1文字にマッチします。範囲指定も可能です(例:[0-9]
)。例:user:[0-9]
はuser:0
からuser:9
にマッチします。[^...]
: ブラケット内の文字以外の任意の1文字にマッチします。
注意点:
MATCH
オプションは、スキャンされた全てのキーに対してパターンマッチングを実行します。これは、COUNT
オプションで指定した数だけ物理的にキーをスキャンし、その中からパターンに合うものを抽出するという動作になります。したがって、MATCH
パターンにマッチするキーが非常に少ない場合でも、多くのキーがスキャンされることになり、特定のパターンにマッチするキーだけを効率的に取得できるわけではありません。全キーをスキャンする過程で、パターンに合うものがあれば返す、という動作に近いイメージです。MATCH
オプションは、SCAN
コマンドの性能に影響を与える可能性があります。特に複雑なパターンや、大量のキーがパターンにマッチする場合、サーバーのCPU使用率が上昇する可能性があります。MATCH
オプションを使用しても、返されるキーのリストが即座にパターンにマッチするキーだけになるわけではありません。COUNT
で指定した数だけキーを走査し、その中からマッチしたものを返します。したがって、一度の応答で返されるキーが全くない場合もあります。
COUNT count
COUNT
オプションは、一度のイテレーションでRedisが内部的にスキャンするハッシュテーブルのバケット数や、そこから取り出す要素数に関するヒントです。デフォルト値は10です。
COUNT
は最小保証ではありません。例えばCOUNT 1000
と指定しても、実際に返されるキーが1000個になるとは限りません。返されるキーの数は、そのイテレーションでスキャンされたバケットに格納されているキーの数や、MATCH
/TYPE
オプションによるフィルタリング結果に依存します。返されるキーは0個の場合もあります。COUNT
はRedisのハッシュテーブルの走査効率に影響を与えます。COUNT
を大きくする:- 一度のラウンドトリップ(クライアント-サーバー間の通信)でより多くのキーを取得できる可能性が高まります。これにより、全キーをスキャンするために必要なラウンドトリップの回数が減り、クライアント側の処理にかかる時間が短縮される可能性があります。
- Redisサーバーが一度に多くのバケットやキーを処理するため、一時的なCPU負荷が高まる可能性があります。ネットワーク帯域もより多く消費します。
COUNT
を小さくする:- サーバーの一時的な負荷は抑えられます。
- 全キーをスキャンするために必要なラウンドトリップの回数が増加します。クライアント側の処理時間(特にネットワークレイテンシが高い場合)が増加する可能性があります。
適切なCOUNT
の値は、ネットワーク環境、Redisサーバーの負荷状況、データベースのキー数や分布など、様々な要因に依存します。一般的には、ある程度大きな値を指定することで、ラウンドトリップ数を減らし全体の効率を上げることが推奨されますが、サーバーの応答が遅延したり、CPU使用率が急増したりしないか、監視しながら調整することが重要です。COUNT
はあくまでヒントであるため、指定した値と実際にスキャンされる数が大きく異なる場合があることを理解しておきましょう。特に、Redisのハッシュテーブルがリサイズされる過程では、COUNT
のヒントから大きく外れた数のキーが処理されることがあります。
TYPE type
TYPE
オプションを使用すると、指定されたデータ型を持つキーのみをスキャンし、結果として返します。
指定可能な型は以下の通りです。
* string
* list
* set
* zset
(sorted set)
* hash
* stream
* module
(カスタムモジュールが定義した型)
例:
redis
SCAN 0 TYPE hash
これは、データベース内のハッシュ型のキーのみをスキャンします。
利点:
- 特定の型のキーだけが必要な場合に非常に効率的です。不要な型のキーをフィルタリングする手間が省けます。
MATCH
オプションと組み合わせて、特定のパターンを持ち、かつ特定の型であるキーを検索できます。
redis
SCAN 0 MATCH user:* TYPE hash
これは、パターンuser:*
にマッチし、かつハッシュ型であるキーのみをスキャンします。
注意点:
TYPE
オプションも、スキャンされた全てのキーに対して型チェックを実行します。これは、MATCH
オプションと同様に、COUNT
のヒントに基づいて内部的にスキャンされたキーの中から、指定された型に合致するものを抽出するという動作になります。TYPE
オプションを使用した場合も、一度の応答で返されるキーが0個になる可能性はあります。
TYPE
オプションは、特に大規模なデータセットで特定の型のキーを対象に操作を行いたい場合に非常に役立ちます。例えば、全てのセッションデータを(ハッシュ型で格納しているとして)取得したい場合などに有効です。
SCAN
ファミリーコマンド:Set, Hash, Sorted Setの要素をスキャンする
SCAN
コマンドはデータベースのキー空間をスキャンしますが、Redisの複合データ型(Set, Hash, Sorted Set)の内部要素についても、同様にカーソルベースでイテレーションを行うための専用コマンドが用意されています。これらはSCAN
ファミリーコマンドと呼ばれます。
SSCAN
:Setの要素をスキャンする
SSCAN key cursor [MATCH pattern] [COUNT count]
指定されたSet型のキーのメンバー(要素)をイテレーションします。
* key
: スキャン対象のSet型のキー名。
* cursor
: 前回のSSCAN
で返されたカーソル(初回は0
)。
* オプション: MATCH pattern
, COUNT count
はSCAN
コマンドと同様です。ただし、MATCH
はSetのメンバーに対してパターンマッチングを行います。
戻り値は2つの要素を持つ配列です。
1. new_cursor
: 次のイテレーションで使用すべき新しいカーソル。完了時は0
。
2. members
: 現在のイテレーションでスキャンされたSetのメンバーのリスト。
例:
“`redis
Set ‘myset’ に要素を追加
SADD myset a b c d e f g h i j k l m n o p q r s t u v w x y z
SSCANで要素をイテレーション (COUNT 5のヒント付き)
127.0.0.1:6379> SSCAN myset 0 COUNT 5
1) “14” # 新しいカーソル
2) 1) “g” # 要素リスト
2) “p”
3) “l”
4) “a”
5) “k”
“`
Setは順序を保持しないため、返される要素の順番は不定です。
HSCAN
:Hashのフィールドと値をスキャンする
HSCAN key cursor [MATCH pattern] [COUNT count]
指定されたHash型のキーのフィールドと値のペアをイテレーションします。
* key
: スキャン対象のHash型のキー名。
* cursor
: 前回のHSCAN
で返されたカーソル(初回は0
)。
* オプション: MATCH pattern
, COUNT count
はSCAN
コマンドと同様です。ただし、MATCH
はHashのフィールド名に対してパターンマッチングを行います。
戻り値は2つの要素を持つ配列です。
1. new_cursor
: 次のイテレーションで使用すべき新しいカーソル。完了時は0
。
2. field_value_pairs
: 現在のイテレーションでスキャンされたフィールドと値のペアのリスト。フィールド名と値が交互に格納された配列として返されます。
例:
“`redis
Hash ‘myhash’ にフィールドと値を追加
HMSET myhash field1 value1 field2 value2 field3 value3 field4 value4
HSCANでフィールド/値ペアをイテレーション
127.0.0.1:6379> HSCAN myhash 0
1) “0” # 新しいカーソル (要素が少ないため1回のイテレーションで完了)
2) 1) “field1” # 要素リスト (フィールド名と値が交互)
2) “value1”
3) “field2”
4) “value2”
5) “field3”
6) “value3”
7) “field4”
8) “value4”
“`
Hashも順序を保持しないため、返されるフィールド/値ペアの順番は不定です。
ZSCAN
:Sorted Setのメンバーとスコアをスキャンする
ZSCAN key cursor [MATCH pattern] [COUNT count]
指定されたSorted Set型のキーのメンバーとスコアのペアをイテレーションします。
* key
: スキャン対象のSorted Set型のキー名。
* cursor
: 前回のZSCAN
で返されたカーソル(初回は0
)。
* オプション: MATCH pattern
, COUNT count
はSCAN
コマンドと同様です。ただし、MATCH
はSorted Setのメンバーに対してパターンマッチングを行います。
戻り値は2つの要素を持つ配列です。
1. new_cursor
: 次のイテレーションで使用すべき新しいカーソル。完了時は0
。
2. member_score_pairs
: 現在のイテレーションでスキャンされたメンバーとスコアのペアのリスト。メンバーとスコアが交互に格納された配列として返されます。スコアは文字列として返されます。
例:
“`redis
Sorted Set ‘myzset’ にメンバーとスコアを追加
ZADD myzset 10 “member:a” 20 “member:b” 30 “member:c” 40 “member:d”
ZSCANでメンバー/スコアペアをイテレーション (MATCH member:*)
127.0.0.1:6379> ZSCAN myzset 0 MATCH member:*
1) “0” # 新しいカーソル (要素が少ないため1回のイテレーションで完了)
2) 1) “member:a” # 要素リスト (メンバーとスコアが交互)
2) “10”
3) “member:b”
4) “20”
5) “member:c”
6) “30”
7) “member:d”
8) “40”
``
ZSCAN`のイテレーション順序はスコア順ではありません。内部的な構造の走査順序に従います。
Sorted Setはスコアによる順序を保持しますが、
SCAN
ファミリーコマンドは、Redisに格納されている複合データ型の内部構造を、SCAN
と同様のノンブロッキングな方法で探索するために非常に重要です。これらのコマンドを使用することで、Set, Hash, Sorted Setに含まれる大量の要素に対しても、サーバーに大きな負荷をかけることなく処理を行うことができます。
SCAN
の動作原理と内部実装(簡易版)
SCAN
コマンドがどのようにノンブロッキングなイテレーションを実現しているのかを理解するためには、Redisがキーや複合データ型の要素をどのように内部で管理しているかを知る必要があります。
Redisのメインデータベース(キー空間)は、ハッシュテーブル(正確には、ディクショナリ構造)で実装されています。このハッシュテーブルは、複数の「バケット」と呼ばれるスロットの配列で構成されており、各バケットにはキーとその値(または値へのポインタ)が格納されています。
SCAN
コマンドのカーソルは、このハッシュテーブルのバケット配列内のインデックスのようなものだと考えることができます(ただし、実際のカーソル値とバケットインデックスは直接対応するわけではありません)。SCAN
は、指定されたカーソル位置から開始し、内部的にいくつかのバケットを順番に走査します。COUNT
オプションは、この走査するバケット数やそこから要素を取り出す量のヒントとなります。
ハッシュテーブルのリサイズとSCAN
の挙動
Redisのハッシュテーブルは、格納されるキーの数が増減するにつれて、効率を維持するために動的にサイズが変更(リサイズ)されます。リサイズは通常、バックグラウンドで段階的に行われます(”incremental rehashing”)。新しいハッシュテーブルが作成され、古いハッシュテーブルのバケットから新しいテーブルへ少しずつキーが移動されます。
SCAN
コマンドは、このリサイズ中のハッシュテーブルに対しても動作します。このとき、SCAN
は古いテーブルと新しいテーブルの両方を走査します。このリサイズ処理と並行してSCAN
が実行されることにより、いくつかの注意点が発生します。
- 要素の重複: リサイズ中に、まだ古いテーブルに残っている要素と、新しいテーブルに既に移動した同じ要素の両方がスキャンされてしまう可能性があります。
- 要素のスキップ: ごくまれに、リサイズのごく特定のタイミングで要素がスキップされる可能性もゼロではありません。
これらの特性のため、SCAN
コマンドによるイテレーションはスナップショットではありません。イテレーション中にデータが変更(追加、削除、更新)された場合、それらの変更がイテレーション結果に反映されたり、前述の重複やスキップが発生したりする可能性があります。
アトミックではない性質
SCAN
コマンド全体(カーソル0
から開始して0
に戻るまでの一連のイテレーション)はアトミックではありません。各SCAN
コマンドの呼び出し自体はアトミックですが、一連のイテレーション全体として見ると、その途中で他のクライアントがデータの読み書きを行う可能性があります。
このアトミックではない性質と、イテレーション中のデータ変更、そしてハッシュテーブルのリサイズ処理によって、SCAN
コマンドは以下の特性を持ちます。
- 全要素の網羅保証なし: 基本的には全ての要素をスキャンするように設計されていますが、リサイズ中のデータ変更によっては、ごくまれに要素がスキップされる可能性があります。
- 要素の重複可能性あり: イテレーション中に要素が移動(リサイズ)または追加された場合、重複して取得される可能性があります。
- イテレーション順序は不定: ハッシュテーブルの内部的な構造によって決定されるため、要素が追加された順やキー名/スコア順など、特定の論理的な順序で返されるわけではありません。
これらの特性は、SCAN
を使用する上で非常に重要です。もし「絶対に重複なく、漏れなく、スキャン開始時点での正確なスナップショットを取得したい」という要件がある場合は、SCAN
は適していません(そもそもRedis自体がそのようなスナップショット取得を目的としたデータベースではありません)。SCAN
はあくまで、「サーバーに負荷をかけずに、データベース全体(あるいは複合データ型の内部)を探索するための手段」と理解するべきです。重複や取得漏れが許容できない処理を行う場合は、スキャン結果を後処理でフィルタリングしたり、データベースを一時停止したり(ただしこれはKEYS
と同様の問題を引き起こす可能性がある)などの追加の考慮が必要です。
SCAN
を使う上での注意点とベストプラクティス
SCAN
コマンドはRedisの運用において非常に有用ですが、その特性を理解せずに使用すると予期せぬ問題を引き起こす可能性があります。ここでは、SCAN
を使う上での注意点とベストプラクティスを解説します。
イテレーションの完了保証:カーソルが0になるまで繰り返す
最も基本的ながら重要な点です。データベース全体(または複合データ型の全要素)をスキャンするには、必ずカーソル0
から開始し、コマンドの戻り値として返されたカーソルが再び0
になるまで、イテレーションを繰り返す必要があります。途中でイテレーションを中断した場合、全てをスキャンしたことにはなりません。
データ変更中のスキャン:重複とスキップの可能性を理解する
前述のように、SCAN
のイテレーション中にデータベースのデータが追加、変更、削除されると、結果に重複やスキップが発生する可能性があります。
- 追加された要素: イテレーションの途中、あるいはこれからスキャンされるバケットに新しい要素が追加された場合、それはスキャン結果に含まれる可能性があります。
- 削除された要素: イテレーションの途中、あるいは既にスキャンされたバケットから要素が削除された場合、それはスキャン結果に含まれなくなります(既に取得済みの場合は手元には残ります)。これからスキャンされるバケットから要素が削除された場合、当然スキャン結果には含まれません。
- 変更された要素: キーの値などが変更されても、通常は
SCAN
の結果(キー名や複合型のメンバー/フィールド名)には直接影響しません。しかし、複合型でキーやメンバー自体が削除・追加されたり、ハッシュテーブルのリサイズが発生したりする場合は、前述の重複・スキップの可能性があります。
これらの挙動は、SCAN
がデータベースのスナップショットを取得するのではなく、あくまで現在のデータベースの状態を「ゆるやかに」走査していることに起因します。もし、取得したキー/要素に対して後続処理を行う場合、その処理が重複に対応できるか、あるいは存在しない要素に対する操作を安全に行えるか(例: GET key
が nil
を返しても大丈夫か)などを考慮する必要があります。
性能への影響:サーバー負荷とCOUNT
の調整
SCAN
コマンドはKEYS
コマンドのようなブロッキングは起こしませんが、全く負荷がかからないわけではありません。
- CPU使用率:
SCAN
は、スキャンするバケットや要素数に応じてCPUを使用します。特にCOUNT
を大きくしたり、MATCH
パターンが複雑だったり、対象となるキー/要素が多い場合は、一時的にCPU使用率が上昇する可能性があります。 - ネットワーク帯域: 一度に多くの要素を返す設定(大きな
COUNT
)にすると、ネットワーク帯域の消費量が増加します。
これらの性能影響を最小限に抑えるためには、以下の点を考慮してください。
- 適切な
COUNT
値の選定: 環境や要件に応じてCOUNT
値を調整します。まずはデフォルト値(10)や少し大きめの値(100〜1000程度)から試してみて、サーバーのCPU負荷やネットワーク使用量、クライアント側のスキャン完了までの時間などを監視しながら最適な値を見つけます。大量のキーを持つデータベースでは、COUNT
を数百〜数千程度に設定することが一般的です。 MATCH
オプションの利用: 特定のパターンにマッチするキーだけが必要な場合はMATCH
を利用しますが、これがサーバー側でフィルタリングを行うため、その処理コストが発生します。TYPE
オプションの活用: 特定の型のキーだけが必要な場合は、必ずTYPE
オプションを指定します。これにより、不要な型のキーをクライアント側でフィルタリングする手間が省け、ネットワーク帯域の節約にもつながります。- 実行頻度と実行タイミング:
SCAN
処理は、サーバーのピーク時間帯を避けて実行したり、スキャン処理自体を低優先度のバックグラウンドジョブとして実行したりするなど、運用上の考慮が必要です。
クライアント側の実装:カーソル管理とエラー処理
SCAN
を安全かつ確実に実行するためには、クライアント側で以下の点を適切に実装する必要があります。
- カーソル管理: 前回の
SCAN
コマンドで返されたカーソル値を正確に保持し、次のコマンドに渡す必要があります。カーソルは数値(文字列として返される)ですが、多くのクライアントライブラリはこれを自動的に処理してくれます。 - ループ処理: カーソルが
0
になるまで、SCAN
コマンドを繰り返し実行するループを適切に実装します。 - エラー処理: ネットワークエラーやRedisサーバー側のエラー(例:
OOM command not allowed when used memory > 'maxmemory'
)が発生した場合の処理を実装します。エラーが発生した場合、イテレーションを中断し、適切なログ出力や再試行処理などを行う必要があります。ただし、エラーから回復してイテレーションを再開する場合、中断したカーソル値から再開することで、既にスキャンした部分をスキップできる可能性があります(完全に保証されるわけではありません)。 - 取得した要素のバッファリング/処理: 各イテレーションで返される要素は一時的なリストです。これらの要素を全て集めて処理したい場合は、クライアント側でリストに追記したり、ファイルに書き出したりするなどのバッファリングや逐次処理が必要です。大量の要素をメモリに保持すると、クライアント側のメモリを圧迫する可能性があるため注意が必要です。
多くのRedisクライアントライブラリは、SCAN
イテレーションを容易にするためのヘルパー関数やイテレーターを提供しています。これらを活用することで、カーソル管理やループ処理の実装を簡略化できます。
大規模データセットでの運用
非常に大規模なデータセット(数億以上のキー)に対してSCAN
を実行する場合、単にループを回すだけでなく、さらに高度な考慮が必要になることがあります。
- 進捗の保存: スキャン処理に非常に時間がかかる場合、途中で処理が中断されたときに、どこまでスキャンが進んだかを保存しておくことが望ましいです。これにより、中断箇所から再開し、最初からやり直すのを避けることができます。返されたカーソル値を外部ストレージ(ファイル、別のRedisインスタンスなど)に保存しておけば、再開時にそのカーソル値を指定して
SCAN
を実行できます。 - 分散処理: スキャン処理自体を複数のワーカーに分散させることも理論的には可能ですが、これはRedisの内部的なハッシュテーブル構造とカーソルシステムを深く理解している必要があり、実装は複雑になります。通常は単一クライアントからのイテレーションで十分です。
- レプリカでの実行: 読み取り操作である
SCAN
は、マスターサーバーではなくレプリカサーバーで実行することで、マスターサーバーへの負荷を軽減できます。ただし、レプリケーション遅延がある場合、レプリカ上のデータはマスターの最新状態を反映していない可能性があるため、スキャン結果もマスターの瞬間の状態とは異なる可能性があることに留意が必要です。
SCAN
とKEYS
の比較:なぜSCAN
が推奨されるのか
改めて、SCAN
とKEYS
コマンドの比較を通じて、なぜSCAN
が推奨されるのかを明確にします。
特徴 | KEYS pattern |
SCAN cursor [options] |
---|---|---|
ブロッキング | 完全にブロッキング(データベースサイズに比例) | ノンブロッキング(一度のスキャン量はCOUNT や内部構造に依存) |
性能影響 | 大規模データセットでは致命的な遅延を引き起こす | COUNT に応じたCPU使用、ネットワーク帯域消費。調整可能。 |
アトミック性 | 単一コマンドなのでアトミック | イテレーション全体としてはアトミックではない |
スナップショット性 | 実行時点のスナップショット(ただしブロッキング) | スナップショットではない(イテレーション中の変更を反映) |
全要素網羅 | 保証される | 基本的に網羅するが、データ変更やリサイズ中に重複/スキップの可能性あり |
返される要素数 | マッチする全要素を一度に返す | 一度の応答で返される要素数は不定 |
メモリ使用 | サーバー側で全結果をバッファするため大量のメモリを消費する可能性あり | 各イテレーションの小さな結果セットのみを返すため、サーバーメモリ消費は限定的 |
用途 | 開発/テスト環境、ごく小規模なデータセットのみ | 本番環境での安全な探索、メンテナンス、棚卸し |
KEYS
コマンドは、小規模な開発/テスト環境で一時的に全キーを確認したい場合などに限定して使用すべきです。本番環境でKEYS
コマンドを実行することは、Redisサーバーの安定性やアプリケーションの可用性に深刻な影響を与える可能性があるため、絶対に避けるべきです。
一方、SCAN
コマンドは、データベース全体の規模に関わらず、オンライン中に安全にキーや要素を探索するための標準的な方法です。データ変更による結果の不確実性(重複・スキップ)はありますが、サーバーをブロッキングしないという最大の利点が、本番環境での利用においてSCAN
を唯一の選択肢としています。
実践的な利用シナリオ
SCAN
およびSCAN
ファミリーコマンドは、様々な運用・開発シナリオで活用できます。
- 全キー/特定パターンキーの棚卸し・一覧取得: データベースにどのようなキーが存在するか、あるいは特定の命名規則に従ったキーがどれだけあるかを知りたい場合に利用します。取得したキーリストをファイルに保存したり、別のシステムで処理したりします。
- 特定の型のキーに対する一括処理: 例えば、全てのHash型のキーに対して何か処理を行いたい場合(例: 構造のチェック、特定のフィールドの集計など)、
SCAN 0 TYPE hash
でHash型のキーを効率的に取得し、それぞれのキーに対してHGETALL
やHSCAN
などで内部を処理します。 - 期限切れキーの特定(参考): Redisはキーの有効期限(TTL)を自動的に管理しますが、明示的に期限切れ間近のキーや期限切れになってしまったキーを特定したい場合に、
SCAN
でキー名を取得し、それぞれのTTL
をチェックするという方法が考えられます。ただし、これも全てのキーをチェックするためには時間がかかり、TTL情報はリアルタイムに変動するため、あくまで補助的な手段と考えた方が良いでしょう。Redisの自動的なキー削除機構に任せるのが一般的です。 - キャッシュウォーミング: アプリケーション起動時に、よく使われるデータを事前にキャッシュにロードする「キャッシュウォーミング」処理において、特定のパターンを持つキーを
SCAN
で取得し、それらのデータを読み込むという使い方が考えられます。 - データのエクスポート/移行: Redisインスタンス間でデータを移行する際や、データを外部ストレージにエクスポートする際に、
SCAN
でキーや要素を抽出し、逐次処理することが可能です。DUMP
コマンドとRESTORE
コマンドも移行には使えますが、データベース全体または特定のキーセットを細かく制御してエクスポートしたい場合にSCAN
が役立ちます。 - インデックスの再構築やメンテナンス: セカンダリインデックスなどを自前で構築している場合、元データ(Redis内のキーや要素)の変更に合わせてインデックスを更新する必要があります。
SCAN
を使用して元データを定期的に走査し、インデックスとの整合性をチェックしたり、インデックスを再構築したりする処理の起点とすることができます。 - Set, Hash, Sorted Setの要素に対する一括処理: 特定のSetに含まれる全ユーザーIDを取得して何か処理を行いたい、Hashに格納された全ユーザープロファイルデータをチェックしたい、Sorted Setの全ランキングメンバーを処理したい、といった場合に、
SSCAN
,HSCAN
,ZSCAN
が役立ちます。
SCAN
コマンドの実装例(Python)
ここでは、代表的なRedisクライアントライブラリであるredis-py
を使ったSCAN
イテレーションのPython実装例を示します。他の言語のライブラリでも、同様のイテレーションパターン(カーソル管理とループ)で実装できます。
“`python
import redis
Redisへの接続設定
実際の接続情報に置き換えてください
r = redis.StrictRedis(host=’localhost’, port=6379, db=0, decode_responses=True) # decode_responses=Trueで文字列として取得
def scan_all_keys(redis_client, match_pattern=’‘, count_hint=1000, key_type=None):
“””
Redisインスタンスから全てのキーをSCANコマンドで取得するジェネレータ
:param redis_client: redis.Redis または redis.StrictRedis インスタンス
:param match_pattern: MATCHオプションのパターン (デフォルト: ‘‘)
:param count_hint: COUNTオプションのヒント値 (デフォルト: 1000)
:param key_type: TYPEオプションの型 (例: ‘hash’, ‘set’など。Noneの場合は全型)
:return: キー名を一つずつyieldするジェネレータ
“””
cursor = 0
while True:
# SCANコマンドを実行
# key_typeが指定されていればtypeオプションを追加
scan_kwargs = {‘match’: match_pattern, ‘count’: count_hint}
if key_type:
scan_kwargs[‘type’] = key_type
response = redis_client.scan(cursor=cursor, **scan_kwargs)
# 戻り値のパース: [新しいカーソル, キーのリスト]
cursor = int(response[0])
keys = response[1] # decode_responses=True なので文字列リスト
# 取得したキーを一つずつyield
for key in keys:
yield key
# カーソルが0になったら終了
if cursor == 0:
break
def scan_hash_fields(redis_client, key, match_pattern=’*’, count_hint=100):
“””
Hash型のキーのフィールドと値のペアをHSCANコマンドで取得するジェネレータ
:param redis_client: redis.Redis または redis.StrictRedis インスタンス
:param key: HSCAN対象のHashキー名
:param match_pattern: MATCHオプションのパターン (フィールド名に対してマッチ)
:param count_hint: COUNTオプションのヒント値
:return: (フィールド名, 値) のペアを一つずつyieldするジェネレータ
“””
cursor = 0
while True:
response = redis_client.hscan(name=key, cursor=cursor, match=match_pattern, count=count_hint)
cursor = int(response[0])
items = response[1] # {field: value, ...} 形式の辞書
# 取得したフィールド/値ペアを一つずつyield
for field, value in items.items():
yield (field, value)
if cursor == 0:
break
— 使用例 —
全てのキーをスキャンして表示
print(“Scanning all keys:”)
try:
for key in scan_all_keys(r, count_hint=500):
print(f” Found key: {key}”)
except Exception as e:
print(f”An error occurred during SCAN: {e}”)
特定のパターンを持つキーをスキャンして表示
print(“\nScanning keys matching ‘user:‘:”)
try:
for key in scan_all_keys(r, match_pattern=’user:‘, count_hint=100):
print(f” Found user key: {key}”)
except Exception as e:
print(f”An error occurred during SCAN: {e}”)
Hash型のキーのみをスキャンして表示
print(“\nScanning hash type keys:”)
try:
for key in scan_all_keys(r, key_type=’hash’, count_hint=200):
print(f” Found hash key: {key}”)
# 見つかったハッシュキーのフィールドをHSCANでスキャンする例 (ネストされたスキャン)
print(f" Scanning fields of hash key: {key}")
try:
for field, value in scan_hash_fields(r, key, count_hint=50):
print(f" Field: {field}, Value: {value}")
except Exception as hs_e:
print(f" An error occurred during HSCAN for key {key}: {hs_e}")
except Exception as e:
print(f”An error occurred during SCAN for hash type: {e}”)
SSCAN, ZSCAN も同様のパターンで実装可能
redis-pyのscan_iter, hscan_iter, sscan_iter, zscan_iter メソッドを使うと、
上記のwhileループとカーソル管理を自動で行ってくれるため、より簡単に記述できます。
scan_iterを使った例
print(“\nScanning all keys using scan_iter:”)
try:
for key in r.scan_iter(match=’*’, count=500):
print(f” Found key (iter): {key}”)
except Exception as e:
print(f”An error occurred during scan_iter: {e}”)
“`
上記のコードでは、ジェネレータ関数としてscan_all_keys
とscan_hash_fields
を実装しています。これにより、スキャンされたキーやフィールドをメモリ上に全て読み込むことなく、一つずつ処理することが可能です。redis-py
のような多くのクライアントライブラリには、scan_iter
のような便利なイテレーターメソッドが組み込まれており、これを利用するとさらに簡潔にSCAN
処理を記述できます。
よくある質問 (FAQ)
Q1: COUNT
を非常に大きくすれば、SCAN
は速くなりますか?
必ずしも速くなるわけではありません。COUNT
はあくまでヒントであり、一度に多くのキーをスキャンしようと試みますが、実際に返されるキーの数は保証されません。COUNT
を大きくしすぎると、一度のリクエストでRedisサーバーのCPU負荷が急増したり、クライアントとサーバー間のネットワーク通信量が増大したりする可能性があります。結果として、サーバーの応答が遅延したり、クライアント側の処理が追いつかなくなったりして、かえって全体のスキャン完了時間が延びたり、安定性が損なわれたりすることがあります。適切なCOUNT
値は、環境(キー数、サーバー性能、ネットワーク)に応じて監視しながら調整する必要があります。
Q2: SCAN
のイテレーション中にキーを削除したり追加したりしても大丈夫ですか?
はい、SCAN
はデータ変更と並行して動作するように設計されています。ただし、前述の通り、イテレーション中に削除されたキーは結果に含まれなくなる可能性があり、追加されたキーは結果に含まれる可能性があります。また、ハッシュテーブルのリサイズとデータ変更が重なると、ごくまれに要素の重複やスキップが発生する可能性があります。SCAN
はスナップショットではないため、このような挙動は仕様の一部です。
Q3: SCAN
はアトミックですか?
いいえ、SCAN
コマンドの「一回の呼び出し」はアトミックですが、「カーソルが0に戻るまでの一連のイテレーション全体」はアトミックではありません。各SCAN
コマンド呼び出しの間で、他のクライアントからのコマンドが実行される可能性があります。
Q4: SCAN
で取得漏れや重複はありますか?
はい、Redisのハッシュテーブルのリサイズ処理とデータ変更が並行して行われる場合、ごくまれに取得漏れ(スキップ)が発生する可能性が理論上あります。より頻繁に発生しうるのは重複です。これはSCAN
の設計上の特性であり、全要素を完全に網羅し、かつ重複がないことを絶対的に保証するものではありません(ただし、通常の使用においては多くの要素を網羅できます)。もし厳密なスナップショットが必要な場合は、他の方法(例えば、RDBファイルを解析するなど)を検討する必要があります。
Q5: SCAN
はマスターで実行すべきですか、レプリカで実行すべきですか?
SCAN
は読み取り操作なので、マスターサーバーへの負荷を分散するためにレプリカサーバーで実行することが推奨されます。ただし、レプリケーション遅延がある場合、レプリカ上のデータはマスターの最新状態ではない可能性があるため、その点は考慮が必要です。ほとんどのユースケースでは、多少の遅延を許容してレプリカで実行する方が、マスターの可用性を維持する上で優れています。
Q6: MATCH
パターンで大文字・小文字を区別しますか?
Redisのキー名自体はバイナリセーフであり、大文字・小文字を含めることができます。MATCH
パターンでのマッチングも、デフォルトでは大文字・小文字を区別します。もし区別したくない場合は、キー名を格納する際に全て小文字にするなどの工夫が必要です。
まとめ
RedisのSCAN
コマンドは、KEYS
コマンドが持つブロッキングの問題を解決し、大規模なデータセットに対しても安全かつ効率的にキーや要素を探索するための不可欠なツールです。カーソルベースのイテレーション、ノンブロッキングな動作、MATCH
、COUNT
、TYPE
といった柔軟なオプションにより、様々なシナリオに対応できます。
SCAN
ファミリーコマンドであるSSCAN
、HSCAN
、ZSCAN
も同様の原則に基づいて、Set、Hash、Sorted Setといった複合データ型の内部要素を安全に走査する方法を提供します。
SCAN
を使用する上では、その特性、特にイテレーション中に発生しうるデータ変更による重複やスキップの可能性、そしてイテレーション全体がアトミックではない性質を理解することが非常に重要です。また、適切なCOUNT
値の選定や、クライアント側での堅牢なカーソル管理とエラー処理の実装が、大規模データセットでの安定運用には欠かせません。
本番環境において、Redisの全キーや特定の要素を探索する必要が生じた場合は、迷わずSCAN
ファミリーコマンドを選択してください。KEYS
コマンドは開発・テスト環境以外では使用しないというルールを徹底することが、Redisサーバーの健全性を維持し、アプリケーションの可用性を確保するための鍵となります。
この記事が、あなたのRedis運用におけるSCAN
コマンドの理解と活用の一助となれば幸いです。
参考資料
- Redis Documentation: SCAN command: https://redis.io/commands/scan/
- Redis Documentation: SSCAN command: https://redis.io/commands/sscan/
- Redis Documentation: HSCAN command: https://redis.io/commands/hscan/
- Redis Documentation: ZSCAN command: https://redis.io/commands/zscan/
- Redis Documentation: How SCAN works: https://redis.io/docs/latest/commands/scan/scan-how-it-works/
- Redis Documentation: Incremental rehashing: https://redis.io/docs/latest/internal/dict/#incremental-rehashing