データ型 | 判定基準 |
String | 単一のValueが10MBを超える |
Hash | 要素数が10,000を超えるか、または総容量が100MBを超える |
List | 要素数が10,000を超えるか、または総容量が100MBを超える |
Set | 要素数が10,000を超えるか、または総容量が100MBを超える |
ZSet | 要素数が10,000を超えるか、または総容量が100MBを超える |
HGETALL操作リクエストを受信します。ZRANGE操作リクエストを受信します。判定ディメンション | 判定方法 |
アクセス集中度 | DBbrainのホットKey分析によりアクセス量ランキングを取得し、ランキング首位のKeyとその後のKeyの間に数量級の差があるかどうかを確認します。 |
相対比率 | 単一Keyのリクエスト数がインスタンスの総リクエスト数に占める割合、および単一Keyのトラフィックがインスタンスの出力帯域幅に占める割合を計算します。割合が高いほどリスクが大きくなります。 |
リソース余裕 | インスタンス仕様に基づき、該当Keyが存在するノードのCPU、帯域幅、接続数が上限に近づいているかどうかを評価します。 |
問題 | 具体的な現象 |
メモリ使用量が不均一です。 | クラスタアーキテクチャにおいて、あるシャードのメモリ使用率が他のシャードを大幅に上回り、maxmemory の上限を引き起こす可能性があり、重要な Key が追い出され、さらには OOM に至る可能性があります。 |
リクエストのブロックによるタイムアウト | コマンド実行はシングルスレッドモデルを採用しており、大きなKeyの操作には時間がかかります(例: DEL、HGETALL)。その間、後続のリクエストはキューで待機するしかありません。 |
同期中断/主従切替 | 大きなKeyの削除または RENAME操作は、マスターノードを長時間ブロックし、マスタースレーブ同期の中断やフェイルオーバーを引き起こす可能性があります。 |
ネットワーク輻輳 | 1MBの大きなKeyに毎秒1,000回アクセスすると、約1GB/sのトラフィックが発生し、インスタンスの帯域幅を占有する可能性があります。 |
問題 | 具体的な現象 |
CPU使用率が継続的に高くなっています。 | ホットリクエストが単一ノードに集中し、当該ノードのCPUがボトルネックとなり、サービス全体の性能が低下します。 |
アクセス傾斜 | ホットKeyが存在するシャードは他のシャードをはるかに超えるアクセス圧力を受け、接続数の枯渇やノード異常が発生する可能性があります。アクセスが単一のKeyに集中しているため、シャードを水平拡張しても当該Keyの圧力を分散することはできません。 |
キャッシュブレイクダウン | ホットKeyの有効期限切れ、またはノード異常が発生した瞬間、高度に集中したトラフィックは直接バックエンドデータベースに到達します。データベースが受け止められない場合、連鎖障害を引き起こす可能性があります。 |
-aパラメータを使用するのではなく、REDISCLI_AUTH環境変数でパスワードを渡すことをお勧めします:# 環境変数でパスワードを渡し、平文の露出を避けてください。export REDISCLI_AUTH='<パスワード>'# 大Keyの調査:データ型ごとに要素数が最も多いKeyをそれぞれ出力します。redis-cli -h <インスタンスアドレス> -p <ポート> --bigkeys# ホットKeyの調査:LFUアクセス頻度統計に基づいて高頻度Keyを出力します。redis-cli -h <インスタンスアドレス> -p <ポート> --hotkeys
SCANカーソルに基づいてすべてのKeyを走査し、メインスレッドを長時間ブロックすることはありません。ただし、データ量が多い場合は追加のリクエストオーバーヘッドが発生するため、業務の低ピーク時に実行することをお勧めします。クラスタアーキテクチャでは、各シャードノードにそれぞれ接続して実行する必要があり、1回の接続では現在のノードのデータのみを統計できます。--bigkeysが統計するのは要素数(Stringはバイト長)であり、実際のメモリ使用量ではありません。そのため、結果は要素規模のみを反映します。メモリ使用量に基づいて特定する必要がある場合は、方法1のメモリ分析機能を使用してください。--hotkeysはLFU淘汰ポリシーに依存するため、使用するには先にインスタンスの淘汰ポリシーパラメータを調整する必要があります。--hotkeysを使用する前に、インスタンスパラメータmaxmemory-policyをallkeys-lfuまたはvolatile-lfuに設定する必要があります。そうしないと、コマンドは直接エラーを報告します。このパラメータはインスタンスのKey追い出し動作を決定します。変更後、既存データの淘汰順序が変わる可能性があるため、業務への影響を評価してから調整してください。jedis.get(key)をHotKeyDetector.get(jedis, key)に置き換えて埋め込みを完了し、ScheduledExecutorServiceを通じて60秒ごとにprintTopN(10)を1回呼び出して結果を出力し、reset()を実行してリセットします。その他のクライアント(Lettuce、Redisson)とコマンド(set、hgetなど)は、このコンポーネントを参照して拡張できます。import redis.clients.jedis.Jedis;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.atomic.AtomicLong;public class HotKeyDetector {// Key → アクセス回数private static final ConcurrentHashMap<String, AtomicLong> KEY_COUNT = new ConcurrentHashMap<>();// Key → 累計所要時間(ナノ秒)private static final ConcurrentHashMap<String, AtomicLong> KEY_TIME = new ConcurrentHashMap<>();/*** Jedis get操作をラップし、各Keyのアクセス回数と所要時間を自動的に統計します。*/public static String get(Jedis jedis, String key) {long start = System.nanoTime();try {return jedis.get(key);} finally {record(key, System.nanoTime() - start);}}private static void record(String key, long nanos) {KEY_COUNT.computeIfAbsent(key, k -> new AtomicLong()).incrementAndGet();KEY_TIME.computeIfAbsent(key, k -> new AtomicLong()).addAndGet(nanos);}/*** TopNのホットKeyを出力します。スケジュールタスク(例:60秒ごと)による呼び出しを推奨します。*/public static void printTopN(int n) {System.out.println("===== Top " + n + " Hot Keys =====");KEY_COUNT.entrySet().stream().sorted((a, b) -> Long.compare(b.getValue().get(), a.getValue().get())).limit(n).forEach(e -> {String key = e.getKey();long count = e.getValue().get();long totalNs = KEY_TIME.getOrDefault(key, new AtomicLong()).get();double avgMs = count > 0 ? (totalNs / 1_000_000.0) / count : 0;System.out.printf("Key: %s | Count: %d | Avg: %.2f ms%n",key, count, avgMs);});}/*** 統計をリセットします(毎回出力後に呼び出し、データの蓄積を防ぎます)。*/public static void reset() {KEY_COUNT.clear();KEY_TIME.clear();}}
方法 | 適用シナリオ | 強み | デメリット |
DBbrainコンソール | 日常的な運用保守、定期的な調査 | 可視化、履歴データ分析に対応、業務コードの変更が不要です。 | コンソールへのアクセス権限が必要です。 |
redis-cli --bigkeys | コマンドライン環境で大容量Keyを迅速に自己点検します。 | 追加のデプロイは不要で、すぐに利用できます。 | 要素数のみをカウントし、メモリ使用量は反映されません。 |
redis-cli --hotkeys | コマンドライン環境でホットKeyを迅速に自己点検します。 | 追加のデプロイは不要で、すぐに利用できます。 | LFU淘汰ポリシーに依存するため、インスタンスパラメータを調整する必要があります。 |
業務層への埋め込み | 正確な位置特定、長期的かつ継続的な監視 | 統計ディメンションはカスタマイズ可能で、ビジネスコンテキストと関連付けることができます。 | コードの改修が必要であり、若干のパフォーマンスオーバーヘッドが生じます。 |
DELコマンドの直接使用を避け、段階的なクリーンアップに切り替えます:# Hash:HSCAN + HDELで期限切れメンバーを段階的に削除し、クリーンアップが完了するまでループ実行します。HSCAN key cursor COUNT 100HDEL key field1 field2 ...# List:LTRIMでバッチ処理によりトリミングし、先頭の1,000件のみを保持します。LTRIM key 0 999# Redis 4.0+:UNLINKで大Key全体を非同期に削除し、ブロッキングを回避します。UNLINK large_key
// 書き込み:圧縮後にバイナリ形式で保存します。// gzipCompress / gzipDecompressは、java.util.zip.GZIPOutputStreamに基づいて独自に実装できます。byte[] compressed = gzipCompress(rawJsonString);jedis.set(key.getBytes(), compressed);// 読み取り:バイナリデータを取り出した後、解凍して復元します。byte[] data = jedis.get(key.getBytes());String rawJson = gzipDecompress(data);
# 分割前user:profile:1001 → 2MB JSON# 分割後(フィールドごとにグループ化)user:profile:1001:basic → 基本情報user:profile:1001:extend → 拡張情報user:profile:1001:settings → 設定情報
# Hash分割:Fieldにハッシュを適用して剰余を求め、N個の子Hashに分散します。# シャード番号 = hash(field) % N、書き込みと読み取りは同じルールで位置を特定します。user:cart:1001:shard_0 → 剰余結果が0のFieldを格納します。user:cart:1001:shard_1 → 剰余結果が1のFieldを格納します。...
MGETなどの複数Keyコマンドはスロットをまたいで実行できない可能性があります。一括読み取りが必要な場合は、ハッシュタグを使用して子Keyを同じシャードに固定する必要があります。例えば、{user:profile:1001}:basic、{user:profile:1001}:extendのように、中括弧内の内容が同じであれば同じスロットに配置されることが保証されます。ただし、ハッシュタグによってこれらのKeyが単一のシャードに集中するため、そのKeyが同時にホットKeyである場合、アクセスの偏りがかえって悪化します。したがって、一括読み取りの要件とシャード分散の要件は、実際のシナリオに基づいてバランスを取る必要があります。# 転送前file:report:2025-q4 → 15MB PDF BLOB# 転送後file:report:2025-q4:url → "https://bucket.cos.region.myqcloud.com/report-2025-q4.pdf"file:report:2025-q4:meta → {"size": 15728640, "md5": "abc123..."}
// Caffeine ローカルキャッシュのホットデータprivate static final LoadingCache<String, String> HOT_KEY_CACHE = Caffeine.newBuilder().maximumSize(1000) // 最大キャッシュエントリ数.expireAfterWrite(50, TimeUnit.MILLISECONDS) // 有効期限、データ不整合の期間を制御します。.build(key -> {// キャッシュミス時にRedisへアクセスし、接続プールから接続を取得します。try (Jedis jedis = JEDIS_POOL.getResource()) {return jedis.get(key);}});
# 元のホットKeyhot:product:1001# 複数レプリカへの分散hot:product:1001:0 → レプリカ0hot:product:1001:1 → レプリカ1hot:product:1001:2 → レプリカ2
// 読み取り時にランダムに1つのレプリカを選択し、リクエストを異なるシャードに分散させます。int index = ThreadLocalRandom.current().nextInt(REPLICA_COUNT);String key = "hot:product:" + productId + ":" + index;try (Jedis jedis = JEDIS_POOL.getResource()) {String value = jedis.get(key);}
ユーザーリクエスト → Nginxローカルキャッシュ → アプリケーションローカルキャッシュ(Caffeine) → Redis → データベース
ZRANGE 0 -1を代替)KEYS *、SMEMBERS、HGETALLなどの全量走査コマンドの使用を禁止し、SCAN、SSCAN、HSCANを使用してバッチで取得してください。PipelineまたはMGET/MSETを優先的に使用し、ループによる単一コマンドの実行を避けてください。UNLINKを使用して非同期で削除します(Redis 4.0+)。フィードバック