Article

🔎 RedisのKEYSとSCANはどう使い分ける?本番で安全にキーを列挙する実装

redis, nodejs, backend, performance

Redisで「cache:product:*に一致するキーを探したい」とき、KEYSとSCANのどちらを使っていますか。

KEYSは簡単ですが、データ量が増えた本番環境で無造作に実行すると、ほかのコマンドの応答を遅らせる可能性があります。一方のSCANは少しずつ列挙できますが、同じキーが複数回返ることがあるなど、独特の注意点があります。

この記事ではRedis公式ドキュメントとNode.js公式クライアントの実装を確認し、運用調査・バッチ処理での使い分けを整理します(確認日:2026年10月14日)。

先に結論

方法 向く用途 注意点
KEYS cache:product:* 小規模な検証環境での調査 全体を一度に走査し、処理中にほかの要求へ影響し得る
SCAN 0 MATCH cache:product:* COUNT 100 本番での段階的な列挙 カーソルが0になるまで繰り返す。重複・空配列に注意
キーを別のSetなどで管理 アプリケーションが頻繁に一覧を取得する場合 インデックスの登録・削除・整合性を設計する

キーの列挙自体が頻繁な要件なら、毎回Redis全体を検索する設計も見直すのがポイントです。

KEYSが本番向きではない理由

例えば、Redis CLI(Redisの操作用コマンドラインツール)で次を実行できます。

KEYS cache:product:*

*は任意の文字列に一致するパターンです。KEYSは条件に合うキーの一覧をまとめて返します。Redisの公式コマンド仕様では、KEYSの計算量はO(N)(格納されているキーの数Nに比例)です。

Redisはコマンド処理に共有の実行資源を使うため、大規模なキー空間の走査は応答遅延につながります。公式ドキュメントでも通常のアプリケーションコードでのKEYS利用を避け、SCANや集合型を検討するよう注意しています。

※少量のローカルデータを調べるときまで禁止する必要はありません。重要なのは環境・件数・実行頻度です。

SCANはカーソルで分割して読む

SCANは一度にすべてのキーを返さず、カーソル(次にどこから読み進めるかを示す値)を使って処理を続けます。

SCAN 0 MATCH cache:product:* COUNT 100
  • 0:最初の読み取りを開始するカーソル。
  • MATCH cache:product:*:取得した候補から該当キー名を絞り込む条件。
  • COUNT 100:各回の作業量を調整するヒント。100件ちょうどを保証しません。

応答は「次のカーソル」と「今回のキー配列」の2要素です。たとえば次のように返ります。

1) "42"
2) 1) "cache:product:100"
   2) "cache:product:205"

続けてSCAN 42 MATCH cache:product:* COUNT 100を呼びます。返されたカーソルが0になったときに一巡が完了します。

MATCHに一致するキーが少ない場合、配列が空でも次のカーソルが0ではないことがあります。「0件返ったから終了」と判定するのは誤りです。

Redisの公式仕様では、SCANの計算量はおおむね1回の呼び出しがO(1)、一巡全体ではO(N)です。負荷がなくなるわけではなく、処理を小分けにできるのが利点です。

redis-cliで少しずつ調べる

Redisに接続できる環境なら、次のコマンドでキー名を列挙できます。

redis-cli --scan --pattern 'cache:product:*'

redis-cliはRedisを操作するCLI、--scanは内部でSCANを繰り返すオプション、--patternは名前を絞る指定です。引用符はシェルが*を勝手に展開しないようにするために付けます。

運用中の本番環境では、まず読み取りだけで確認してください。一致したものを無条件に削除するコマンドへパイプでつなぐのは避けましょう。

Redisの接続先が意図した開発・ステージング・本番のどれなのかも、実行前に明示的に確認します。

Node.jsのバッチ処理例

公式のnode-redisクライアントを使うと、SCANのカーソル管理をscanIterator()に任せられます。

npm install redis

npm installは依存パッケージを追加するコマンド、redisは公式Node.jsクライアントのパッケージ名です。次の例は単一Redisインスタンスを想定します(redis-scan.mjsとして保存)。

import { createClient } from "redis";

const client = createClient({
  url: process.env.REDIS_URL ?? "redis://localhost:6379",
});

client.on("error", (error) => console.error("Redis:", error));
await client.connect();

try {
  for await (const keys of client.scanIterator({
    MATCH: "cache:product:*",
    COUNT: 100,
    TYPE: "string",
  })) {
    // 現行node-redisのscanIteratorはキーの配列を順次返す。
    if (keys.length === 0) continue;

    // この例は値が文字列のキーだけを対象とする。
    const values = await client.mGet(keys);
    for (let i = 0; i < keys.length; i++) {
      if (values[i] === null) continue; // 走査中に期限切れとなる場合
      console.log(keys[i], values[i]);
    }
  }
} finally {
  await client.close();
}

実行するときはnode redis-scan.mjsです。nodeはNode.jsでファイルを実行するコマンドです。REDIS_URL環境変数で接続先を切り替えられます。

TYPE: "string"は文字列型のキーに絞る指定です。ただし、キー名の接頭辞だけでは値の型は決まりません。型が混在する用途では、型ごとに処理を分けます。

mGetは複数の文字列キーを一度に取得するコマンドです。ここでの例は単一インスタンス向けであり、Redis Clusterの複数スロットにまたがる複数キー処理にはそのまま流用しません。

また、クライアントのバージョンによりイテレーターが返す形が異なる場合があります。利用中のredisパッケージのREADMEや型定義を確認してください。

SCANを使っても残る3つの落とし穴

1. 同じキーが複数回返る

SCANはキーが重複して返る可能性があると公式に明記されています。単純なログ出力や読み取りなら許容できる場合もありますが、課金・集計・通知・ジョブ実行でキーを1回ずつ処理したい場合は重複対策が必要です。

処理側を**冪等(べきとう)**にする、つまり同じ入力を繰り返し処理しても結果が壊れない設計にするのが基本です。全キーをSetへ保持して除外する方式はメモリを使うため、大きなデータセットでは慎重に検討します。

2. SCANは一瞬の正確なスナップショットではない

走査中に新たに追加・削除されたキーが、返るかどうかは確定しません。走査の開始から終了まで存在し続けたキーには保証がありますが、「ある時点の全キーを厳密に一覧化する処理」には向きません。

業務上の正確な一覧が必要なら、データ登録時に別の索引を維持する、DBに正本を持つなどの設計が適しています。

3. COUNTは上限ではなくヒント

COUNT 100は100件ずつ固定で返す意味ではありません。0件の回や100件を超える回があり得ます。また、MATCHは検索対象をあらかじめ索引で絞り込むものではありません。極端に一致件数が少ないパターンでは、空の応答が続く場合もあります。

大量のデータを処理するなら、応答時間・CPU使用率・処理間隔を測りながらCOUNTを調整します。多数のバッチを一斉に走らせるのも避けます。

削除処理はUNLINKを理解してから

UNLINK keyはキーを名前空間から外し、主なメモリ解放をバックグラウンドへ任せるコマンドです。DELより大きな値の削除で応答が安定しやすい場合があります。

ただし、UNLINKも削除操作そのものです。SCAN結果を無条件に削除するのではなく、接続先・対象パターン・件数・実行時間帯を確認し、ロールバック方針を決めてから使用します。

まとめ

  • KEYSはデバッグ時に便利。ただし大量データの本番運用では慎重に扱う。
  • SCANはカーソルで分割して列挙し、0へ戻るまで継続する。
  • 空配列、重複、走査中の更新、COUNTの非保証を理解する。
  • 頻繁な厳密一覧が必要なら、SCANで毎回探索せず設計を見直す。

公式参考資料(2026年10月14日確認)