Redis SCANコマンド完全マスターガイド

はい、承知いたしました。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つの要素を持つ配列を返します。

  1. new_cursor: 次のイテレーションで使用すべき新しいカーソル。イテレーションが完了した場合は0が返されます。
  2. 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のカーソルは、このハッシュテーブルの走査位置を示しています。

イテレーションの基本的な流れは以下のようになります。

  1. 最初のSCANコマンドをカーソル0で実行します。
  2. Redisは、現在のカーソル位置から指定されたCOUNTのヒントに基づいて、ハッシュテーブルのバケットをいくつかスキャンします。
  3. スキャンしたバケットから見つかったキーのうち、MATCHTYPEの条件を満たすものを抽出します。
  4. 抽出したキーのリストと、次のイテレーションで使用するための新しいカーソルをクライアントに返します。
  5. クライアントは、返された新しいカーソルを使用して、次のSCANコマンドを実行します。
  6. このプロセスを、返されたカーソルが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:1user: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 countSCANコマンドと同様です。ただし、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 countSCANコマンドと同様です。ただし、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 countSCANコマンドと同様です。ただし、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”
``
Sorted Setはスコアによる順序を保持しますが、
ZSCAN`のイテレーション順序はスコア順ではありません。内部的な構造の走査順序に従います。

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 keynil を返しても大丈夫か)などを考慮する必要があります。

性能への影響:サーバー負荷と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は、マスターサーバーではなくレプリカサーバーで実行することで、マスターサーバーへの負荷を軽減できます。ただし、レプリケーション遅延がある場合、レプリカ上のデータはマスターの最新状態を反映していない可能性があるため、スキャン結果もマスターの瞬間の状態とは異なる可能性があることに留意が必要です。

SCANKEYSの比較:なぜSCANが推奨されるのか

改めて、SCANKEYSコマンドの比較を通じて、なぜSCANが推奨されるのかを明確にします。

特徴 KEYS pattern SCAN cursor [options]
ブロッキング 完全にブロッキング(データベースサイズに比例) ノンブロッキング(一度のスキャン量はCOUNTや内部構造に依存)
性能影響 大規模データセットでは致命的な遅延を引き起こす COUNTに応じたCPU使用、ネットワーク帯域消費。調整可能。
アトミック性 単一コマンドなのでアトミック イテレーション全体としてはアトミックではない
スナップショット性 実行時点のスナップショット(ただしブロッキング) スナップショットではない(イテレーション中の変更を反映)
全要素網羅 保証される 基本的に網羅するが、データ変更やリサイズ中に重複/スキップの可能性あり
返される要素数 マッチする全要素を一度に返す 一度の応答で返される要素数は不定
メモリ使用 サーバー側で全結果をバッファするため大量のメモリを消費する可能性あり 各イテレーションの小さな結果セットのみを返すため、サーバーメモリ消費は限定的
用途 開発/テスト環境、ごく小規模なデータセットのみ 本番環境での安全な探索、メンテナンス、棚卸し

KEYSコマンドは、小規模な開発/テスト環境で一時的に全キーを確認したい場合などに限定して使用すべきです。本番環境でKEYSコマンドを実行することは、Redisサーバーの安定性やアプリケーションの可用性に深刻な影響を与える可能性があるため、絶対に避けるべきです

一方、SCANコマンドは、データベース全体の規模に関わらず、オンライン中に安全にキーや要素を探索するための標準的な方法です。データ変更による結果の不確実性(重複・スキップ)はありますが、サーバーをブロッキングしないという最大の利点が、本番環境での利用においてSCANを唯一の選択肢としています。

実践的な利用シナリオ

SCANおよびSCANファミリーコマンドは、様々な運用・開発シナリオで活用できます。

  1. 全キー/特定パターンキーの棚卸し・一覧取得: データベースにどのようなキーが存在するか、あるいは特定の命名規則に従ったキーがどれだけあるかを知りたい場合に利用します。取得したキーリストをファイルに保存したり、別のシステムで処理したりします。
  2. 特定の型のキーに対する一括処理: 例えば、全てのHash型のキーに対して何か処理を行いたい場合(例: 構造のチェック、特定のフィールドの集計など)、SCAN 0 TYPE hashでHash型のキーを効率的に取得し、それぞれのキーに対してHGETALLHSCANなどで内部を処理します。
  3. 期限切れキーの特定(参考): Redisはキーの有効期限(TTL)を自動的に管理しますが、明示的に期限切れ間近のキーや期限切れになってしまったキーを特定したい場合に、SCANでキー名を取得し、それぞれのTTLをチェックするという方法が考えられます。ただし、これも全てのキーをチェックするためには時間がかかり、TTL情報はリアルタイムに変動するため、あくまで補助的な手段と考えた方が良いでしょう。Redisの自動的なキー削除機構に任せるのが一般的です。
  4. キャッシュウォーミング: アプリケーション起動時に、よく使われるデータを事前にキャッシュにロードする「キャッシュウォーミング」処理において、特定のパターンを持つキーをSCANで取得し、それらのデータを読み込むという使い方が考えられます。
  5. データのエクスポート/移行: Redisインスタンス間でデータを移行する際や、データを外部ストレージにエクスポートする際に、SCANでキーや要素を抽出し、逐次処理することが可能です。DUMPコマンドとRESTOREコマンドも移行には使えますが、データベース全体または特定のキーセットを細かく制御してエクスポートしたい場合にSCANが役立ちます。
  6. インデックスの再構築やメンテナンス: セカンダリインデックスなどを自前で構築している場合、元データ(Redis内のキーや要素)の変更に合わせてインデックスを更新する必要があります。SCANを使用して元データを定期的に走査し、インデックスとの整合性をチェックしたり、インデックスを再構築したりする処理の起点とすることができます。
  7. 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_keysscan_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コマンドが持つブロッキングの問題を解決し、大規模なデータセットに対しても安全かつ効率的にキーや要素を探索するための不可欠なツールです。カーソルベースのイテレーション、ノンブロッキングな動作、MATCHCOUNTTYPEといった柔軟なオプションにより、様々なシナリオに対応できます。

SCANファミリーコマンドであるSSCANHSCANZSCANも同様の原則に基づいて、Set、Hash、Sorted Setといった複合データ型の内部要素を安全に走査する方法を提供します。

SCANを使用する上では、その特性、特にイテレーション中に発生しうるデータ変更による重複やスキップの可能性、そしてイテレーション全体がアトミックではない性質を理解することが非常に重要です。また、適切なCOUNT値の選定や、クライアント側での堅牢なカーソル管理とエラー処理の実装が、大規模データセットでの安定運用には欠かせません。

本番環境において、Redisの全キーや特定の要素を探索する必要が生じた場合は、迷わずSCANファミリーコマンドを選択してください。KEYSコマンドは開発・テスト環境以外では使用しないというルールを徹底することが、Redisサーバーの健全性を維持し、アプリケーションの可用性を確保するための鍵となります。

この記事が、あなたのRedis運用におけるSCANコマンドの理解と活用の一助となれば幸いです。

参考資料


上記で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つの要素を持つ配列を返します。

  1. new_cursor: 次のイテレーションで使用すべき新しいカーソル。イテレーションが完了した場合は0が返されます。
  2. 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のカーソルは、このハッシュテーブルの走査位置を示しています。

イテレーションの基本的な流れは以下のようになります。

  1. 最初のSCANコマンドをカーソル0で実行します。
  2. Redisは、現在のカーソル位置から指定されたCOUNTのヒントに基づいて、ハッシュテーブルのバケットをいくつかスキャンします。
  3. スキャンしたバケットから見つかったキーのうち、MATCHTYPEの条件を満たすものを抽出します。
  4. 抽出したキーのリストと、次のイテレーションで使用するための新しいカーソルをクライアントに返します。
  5. クライアントは、返された新しいカーソルを使用して、次のSCANコマンドを実行します。
  6. このプロセスを、返されたカーソルが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:1user: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 countSCANコマンドと同様です。ただし、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 countSCANコマンドと同様です。ただし、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 countSCANコマンドと同様です。ただし、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”
``
Sorted Setはスコアによる順序を保持しますが、
ZSCAN`のイテレーション順序はスコア順ではありません。内部的な構造の走査順序に従います。

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 keynil を返しても大丈夫か)などを考慮する必要があります。

性能への影響:サーバー負荷と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は、マスターサーバーではなくレプリカサーバーで実行することで、マスターサーバーへの負荷を軽減できます。ただし、レプリケーション遅延がある場合、レプリカ上のデータはマスターの最新状態を反映していない可能性があるため、スキャン結果もマスターの瞬間の状態とは異なる可能性があることに留意が必要です。

SCANKEYSの比較:なぜSCANが推奨されるのか

改めて、SCANKEYSコマンドの比較を通じて、なぜSCANが推奨されるのかを明確にします。

特徴 KEYS pattern SCAN cursor [options]
ブロッキング 完全にブロッキング(データベースサイズに比例) ノンブロッキング(一度のスキャン量はCOUNTや内部構造に依存)
性能影響 大規模データセットでは致命的な遅延を引き起こす COUNTに応じたCPU使用、ネットワーク帯域消費。調整可能。
アトミック性 単一コマンドなのでアトミック イテレーション全体としてはアトミックではない
スナップショット性 実行時点のスナップショット(ただしブロッキング) スナップショットではない(イテレーション中の変更を反映)
全要素網羅 保証される 基本的に網羅するが、データ変更やリサイズ中に重複/スキップの可能性あり
返される要素数 マッチする全要素を一度に返す 一度の応答で返される要素数は不定
メモリ使用 サーバー側で全結果をバッファするため大量のメモリを消費する可能性あり 各イテレーションの小さな結果セットのみを返すため、サーバーメモリ消費は限定的
用途 開発/テスト環境、ごく小規模なデータセットのみ 本番環境での安全な探索、メンテナンス、棚卸し

KEYSコマンドは、小規模な開発/テスト環境で一時的に全キーを確認したい場合などに限定して使用すべきです。本番環境でKEYSコマンドを実行することは、Redisサーバーの安定性やアプリケーションの可用性に深刻な影響を与える可能性があるため、絶対に避けるべきです

一方、SCANコマンドは、データベース全体の規模に関わらず、オンライン中に安全にキーや要素を探索するための標準的な方法です。データ変更による結果の不確実性(重複・スキップ)はありますが、サーバーをブロッキングしないという最大の利点が、本番環境での利用においてSCANを唯一の選択肢としています。

実践的な利用シナリオ

SCANおよびSCANファミリーコマンドは、様々な運用・開発シナリオで活用できます。

  1. 全キー/特定パターンキーの棚卸し・一覧取得: データベースにどのようなキーが存在するか、あるいは特定の命名規則に従ったキーがどれだけあるかを知りたい場合に利用します。取得したキーリストをファイルに保存したり、別のシステムで処理したりします。
  2. 特定の型のキーに対する一括処理: 例えば、全てのHash型のキーに対して何か処理を行いたい場合(例: 構造のチェック、特定のフィールドの集計など)、SCAN 0 TYPE hashでHash型のキーを効率的に取得し、それぞれのキーに対してHGETALLHSCANなどで内部を処理します。
  3. 期限切れキーの特定(参考): Redisはキーの有効期限(TTL)を自動的に管理しますが、明示的に期限切れ間近のキーや期限切れになってしまったキーを特定したい場合に、SCANでキー名を取得し、それぞれのTTLをチェックするという方法が考えられます。ただし、これも全てのキーをチェックするためには時間がかかり、TTL情報はリアルタイムに変動するため、あくまで補助的な手段と考えた方が良いでしょう。Redisの自動的なキー削除機構に任せるのが一般的です。
  4. キャッシュウォーミング: アプリケーション起動時に、よく使われるデータを事前にキャッシュにロードする「キャッシュウォーミング」処理において、特定のパターンを持つキーをSCANで取得し、それらのデータを読み込むという使い方が考えられます。
  5. データのエクスポート/移行: Redisインスタンス間でデータを移行する際や、データを外部ストレージにエクスポートする際に、SCANでキーや要素を抽出し、逐次処理することが可能です。DUMPコマンドとRESTOREコマンドも移行には使えますが、データベース全体または特定のキーセットを細かく制御してエクスポートしたい場合にSCANが役立ちます。
  6. インデックスの再構築やメンテナンス: セカンダリインデックスなどを自前で構築している場合、元データ(Redis内のキーや要素)の変更に合わせてインデックスを更新する必要があります。SCANを使用して元データを定期的に走査し、インデックスとの整合性をチェックしたり、インデックスを再構築したりする処理の起点とすることができます。
  7. 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_keysscan_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コマンドが持つブロッキングの問題を解決し、大規模なデータセットに対しても安全かつ効率的にキーや要素を探索するための不可欠なツールです。カーソルベースのイテレーション、ノンブロッキングな動作、MATCHCOUNTTYPEといった柔軟なオプションにより、様々なシナリオに対応できます。

SCANファミリーコマンドであるSSCANHSCANZSCANも同様の原則に基づいて、Set、Hash、Sorted Setといった複合データ型の内部要素を安全に走査する方法を提供します。

SCANを使用する上では、その特性、特にイテレーション中に発生しうるデータ変更による重複やスキップの可能性、そしてイテレーション全体がアトミックではない性質を理解することが非常に重要です。また、適切なCOUNT値の選定や、クライアント側での堅牢なカーソル管理とエラー処理の実装が、大規模データセットでの安定運用には欠かせません。

本番環境において、Redisの全キーや特定の要素を探索する必要が生じた場合は、迷わずSCANファミリーコマンドを選択してください。KEYSコマンドは開発・テスト環境以外では使用しないというルールを徹底することが、Redisサーバーの健全性を維持し、アプリケーションの可用性を確保するための鍵となります。

この記事が、あなたのRedis運用におけるSCANコマンドの理解と活用の一助となれば幸いです。

参考資料


コメントする

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

上部へスクロール