1. 概要

コレクションは、ほとんどすべての最新のアプリケーションで一般的に見られる重要な構成要素です。 したがって、 Redisが、リスト、セット、ハッシュ、並べ替えられたセットなど、さまざまな一般的なデータ構造を提供しているのは当然のことです。

このチュートリアルでは、特定のパターンに一致する利用可能なすべてのRedisキーを効果的に読み取る方法を学習します。

2. コレクションを探索する

アプリケーションがRedisを使用して、さまざまなスポーツで使用されるボールに関する情報を保存していると想像してみてください。 Redisコレクションから入手できる各ボールに関する情報を確認できるはずです。 簡単にするために、データセットを3つのボールのみに制限します。

  • 重さ160gのクリケットボール
  • 重量450gのサッカー
  • 重量270gのバレーボール

いつものように、最初にRedisコレクションを探索するための素朴なアプローチに取り組んで基本をクリアしましょう。

3. redis-cliを使用したナイーブなアプローチ

コレクションを探索するためにJavaコードを書き始める前に、redis-cliインターフェイスを使用してそれをどのように行うかについて公正な考えを持っている必要があります。 コマンドラインインターフェイスを使用して各コレクションタイプを調べるために、Redisインスタンスがポート6379127.0.0.1で利用可能であると仮定します。

3.1. リンクリスト

まず、を使用して、ballsという名前のRedisリンクリストにsports-name _ ball-weightの形式でデータセットを保存しましょう。 ] rpush コマンド:

% redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> RPUSH balls "cricket_160"
(integer) 1
127.0.0.1:6379> RPUSH balls "football_450"
(integer) 2
127.0.0.1:6379> RPUSH balls "volleyball_270"
(integer) 3

リストへの挿入が成功すると、リストの新しい長さが出力されることがわかります。 ただし、ほとんどの場合、データ挿入アクティビティはわかりません。 その結果、 llen コマンドを使用して、リンクリストの長さを確認できます。

127.0.0.1:6379> llen balls
(integer) 3

リストの長さがすでにわかっている場合は、 lrangeコマンドを使用して、データセット全体を簡単に取得すると便利です。

127.0.0.1:6379> lrange balls 0 2
1) "cricket_160"
2) "football_450"
3) "volleyball_270"

3.2. 設定

次に、データセットをRedisセットに保存することを決定したときに、データセットを探索する方法を見てみましょう。 そのためには、まず sadd コマンドを使用して、ballsという名前のRedisセットにデータセットを入力する必要があります。

127.0.0.1:6379> sadd balls "cricket_160" "football_450" "volleyball_270" "cricket_160"
(integer) 3

おっとっと! コマンドに重複した値がありました。 ただし、セットに値を追加していたので、重複について心配する必要はありません。 もちろん、出力された応答値から追加されたアイテムの数を確認できます。

これで、 smembersコマンドを利用して、すべてのセットメンバーを表示できます

127.0.0.1:6379> smembers balls
1) "volleyball_270"
2) "cricket_160"
3) "football_450"

3.3. ハッシュ

次に、Redisのハッシュデータ構造を使用して、データセットをballsという名前のハッシュキーに格納します。ハッシュのフィールドはスポーツ名で、フィールド値はボールの重量です。 これは、hmsetコマンドを使用して実行できます。

127.0.0.1:6379> hmset balls cricket 160 football 450 volleyball 270
OK

ハッシュに格納されている情報を表示するには、hgetallコマンドを使用できます。

127.0.0.1:6379> hgetall balls
1) "cricket"
2) "160"
3) "football"
4) "450"
5) "volleyball"
6) "270"

3.4. ソートされたセット

一意のメンバー値に加えて、sorted-setsを使用すると、それらの横にスコアを保持できます。 さて、私たちのユースケースでは、メンバーの値としてスポーツの名前を保持し、スコアとしてボールの重量を保持することができます。 zaddコマンドを使用してデータセットを保存しましょう。

127.0.0.1:6379> zadd balls 160 cricket 450 football 270 volleyball
(integer) 3

これで、最初に zcard コマンドを使用してソートされたセットの長さを見つけ、次にzrangeコマンドを使用して完全なセットを調べることができます。

127.0.0.1:6379> zcard balls
(integer) 3
127.0.0.1:6379> zrange balls 0 2
1) "cricket"
2) "volleyball"
3) "football"

3.5. 文字列

また、通常のキー値文字列をアイテムの表面的なコレクションとして見ることもできます。 まず、msetコマンドを使用してデータセットにデータを入力しましょう。

127.0.0.1:6379> mset balls:cricket 160 balls:football 450 balls:volleyball 270
OK

Redisデータベースにある可能性のある残りのキーからこれらのキーを識別できるように、プレフィックス「balls:を追加したことに注意する必要があります。 さらに、この命名戦略により、 keys コマンドを使用して、プレフィックスパターンマッチングを使用してデータセットを探索できます。

127.0.0.1:6379> keys balls*
1) "balls:cricket"
2) "balls:volleyball"
3) "balls:football"

4. ナイーブなJavaの実装

さまざまなタイプのコレクションを探索するために使用できる、関連するRedisコマンドの基本的な考え方を開発したので、コードで手を汚すときが来ました。

4.1. Mavenの依存関係

このセクションでは、実装でRedis用のJedisクライアントライブラリ使用します。

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.2.0</version>
</dependency>

4.2. Redisクライアント

Jedisライブラリには、Redis-CLIの名前に似たメソッドが付属しています。 ただし、Jedis関数呼び出しを内部的に呼び出すラッパーRedisクライアントを作成することをお勧めします。

Jedisライブラリを使用する場合は常に、単一のJedisインスタンスはスレッドセーフではないことに注意する必要があります。 したがって、アプリケーションでJedisリソースを取得するには、ネットワーク接続のスレッドセーフプールであるJedisPoolを利用できます。

また、アプリケーションのライフサイクル中の任意の時点でRedisクライアントの複数のインスタンスが浮かんでいることを望まないため、シングルトンデザインパターンの原則に基づいてRedisClientクラスを作成する必要があります。

まず、RedisClientクラスのインスタンスが作成されたときにJedisPoolを内部的に初期化するクライアント用のプライベートコンストラクターを作成しましょう。

private static JedisPool jedisPool;

private RedisClient(String ip, int port) {
    try {
        if (jedisPool == null) {
            jedisPool = new JedisPool(new URI("http://" + ip + ":" + port));
        }
    } catch (URISyntaxException e) {
        log.error("Malformed server address", e);
    }
}

次に、シングルトンクライアントへのアクセスポイントが必要です。 それでは、この目的のために静的メソッド getInstance()を作成しましょう。

private static volatile RedisClient instance = null;

public static RedisClient getInstance(String ip, final int port) {
    if (instance == null) {
        synchronized (RedisClient.class) {
            if (instance == null) {
                instance = new RedisClient(ip, port);
            }
        }
    }
    return instance;
}

最後に、Jedisのlrangeメソッドの上にラッパーメソッドを作成する方法を見てみましょう。

public List lrange(final String key, final long start, final long stop) {
    try (Jedis jedis = jedisPool.getResource()) {
        return jedis.lrange(key, start, stop);
    } catch (Exception ex) {
        log.error("Exception caught in lrange", ex);
    }
    return new LinkedList();
}

もちろん、同じ戦略に従って、 lpush hmset hgetall saddなどの残りのラッパーメソッドを作成できます。 、メンバーキー zadd 、およびzrange

4.3. 分析

コレクションを一度に探索するために使用できるすべてのRedisコマンドは、最良の場合では当然O(n)時間計算量になります。

私たちはおそらく少しリベラルで、このアプローチをナイーブと呼んでいます。 Redisの実際の本番インスタンスでは、1つのコレクションに数千または数百万のキーが含まれるのが一般的です。 さらに、 Redisのシングルスレッドの性質はより多くの悲惨さをもたらし、私たちのアプローチは他のより優先度の高い操作を壊滅的にブロックする可能性があります。

したがって、単純なアプローチをデバッグ目的でのみ使用するように制限していることを強調する必要があります。

5. イテレータの基本

単純な実装の主な欠陥は、Redisに単一のフェッチクエリのすべての結果を一度に提供するように要求していることです。 この問題を解決するために、元のフェッチクエリを、データセット全体のより小さなチャンクで動作する複数のシーケンシャルフェッチクエリに分割できます。

私たちが読むことになっている1,000ページの本があると仮定しましょう。 素朴なアプローチに従うと、この大きな本を一気に読む必要があります。 それは私たちのエネルギーを消耗し、私たちが他のより優先度の高い活動をすることを妨げるので、それは私たちの幸福に致命的です。

もちろん、正しい方法は、複数の読書セッションで本を完成させることです。 各セッションで、前のセッションで中断したところから再開します —ページブックマークを使用して進行状況を追跡できます。

両方の場合の合計読み取り時間は同等の値になりますが、それでも、2番目のアプローチは、呼吸する余地があるため、より優れています。

イテレータベースのアプローチを使用してRedisコレクションを探索する方法を見てみましょう。

6. Redisスキャン

Redisは、カーソルベースのアプローチを使用してコレクションからキーを読み取るためのいくつかのスキャン戦略を提供します。これは、原則として、ページのブックマークに似ています。

6.1. スキャン戦略

Scan コマンドを使用して、キー値コレクションストア全体をスキャンできます。 ただし、コレクションタイプによってデータセットを制限する場合は、次のいずれかのバリアントを使用できます。

  • Sscan は、セットの反復に使用できます
  • Hscan は、ハッシュ内のフィールド値のペアを反復処理するのに役立ちます
  • Zscan は、ソートされたセットに格納されているメンバーの反復を許可します

リンクリスト用に特別に設計されたサーバー側のスキャン戦略は実際には必要ないことに注意する必要があります。 これは、lindexまたはlrangeコマンドを使用してインデックスを介してリンクリストのメンバーにアクセスできるためです。 さらに、要素の数を調べ、 lrange を単純なループで使用して、リスト全体を小さなチャンクで反復することができます。

SCAN コマンドを使用して、文字列タイプのキーをスキャンしてみましょう。 スキャンを開始するには、カーソル値を「0」として使用し、パターン文字列を「ball*」と一致させる必要があります。

127.0.0.1:6379> mset balls:cricket 160 balls:football 450 balls:volleyball 270
OK
127.0.0.1:6379> SCAN 0 MATCH ball* COUNT 1
1) "2"
2) 1) "balls:cricket"
127.0.0.1:6379> SCAN 2 MATCH ball* COUNT 1
1) "3"
2) 1) "balls:volleyball"
127.0.0.1:6379> SCAN 3 MATCH ball* COUNT 1
1) "0"
2) 1) "balls:football"

スキャンが完了するたびに、後続の反復で使用されるカーソルの次の値が取得されます。 最終的に、次のカーソル値が「0」のときにコレクション全体をスキャンしたことがわかります。

7. Javaでスキャンする

これで、Javaでの実装を開始できるように、アプローチについて十分に理解できました。

7.1. スキャン戦略

Jedis クラスが提供するコアスキャン機能を覗いてみると、さまざまなコレクションタイプをスキャンするための戦略が見つかります。

public ScanResult<String> scan(final String cursor, final ScanParams params);
public ScanResult<String> sscan(final String key, final String cursor, final ScanParams params);
public ScanResult<Map.Entry<String, String>> hscan(final String key, final String cursor,
  final ScanParams params);
public ScanResult<Tuple> zscan(final String key, final String cursor, final ScanParams params);

Jedis は、スキャンを効果的に制御するために、 search-patternとresult-sizeの2つのオプションパラメーターを必要とします–ScanParamsはこれを実現します。 この目的のために、[X13X]ビルダーデザインパターンに大まかに基づいている match()および count()メソッドに依存しています。

public ScanParams match(final String pattern);
public ScanParams count(final Integer count);

Jedisのスキャンアプローチに関する基本的な知識を身に付けたので、ScanStrategyインターフェイスを介してこれらの戦略をモデル化しましょう。

public interface ScanStrategy<T> {
    ScanResult<T> scan(Jedis jedis, String cursor, ScanParams scanParams);
}

まず、最も単純な scan 戦略に取り組みましょう。これは、コレクションタイプに依存せず、キーを読み取りますが、キーの値は読み取りません。

public class Scan implements ScanStrategy<String> {
    public ScanResult<String> scan(Jedis jedis, String cursor, ScanParams scanParams) {
        return jedis.scan(cursor, scanParams);
    }
}

次に、 hscan 戦略を取り上げましょう。これは、特定のハッシュキーのすべてのフィールドキーとフィールド値を読み取るように調整されています。

public class Hscan implements ScanStrategy<Map.Entry<String, String>> {

    private String key;

    @Override
    public ScanResult<Entry<String, String>> scan(Jedis jedis, String cursor, ScanParams scanParams) {
        return jedis.hscan(key, cursor, scanParams);
    }
}

最後に、セットとソートされたセットの戦略を作成しましょう。 sscan 戦略では、セットのすべてのメンバーを読み取ることができますが、 zscan 戦略では、メンバーとそのスコアを Tuplesの形式で読み取ることができます。

public class Sscan implements ScanStrategy<String> {

    private String key;

    public ScanResult<String> scan(Jedis jedis, String cursor, ScanParams scanParams) {
        return jedis.sscan(key, cursor, scanParams);
    }
}

public class Zscan implements ScanStrategy<Tuple> {

    private String key;

    @Override
    public ScanResult<Tuple> scan(Jedis jedis, String cursor, ScanParams scanParams) {
        return jedis.zscan(key, cursor, scanParams);
    }
}

7.2. Redisイテレータ

次に、RedisIteratorクラスを構築するために必要なビルディングブロックをスケッチしてみましょう。

  • 文字列ベースのカーソル
  • scan 、sscan、 hscan zscanなどのスキャン戦略
  • パラメータをスキャンするためのプレースホルダー
  • JedisPool にアクセスして、Jedisリソースを取得します

これで、RedisIteratorクラスでこれらのメンバーを定義できます。

private final JedisPool jedisPool;
private ScanParams scanParams;
private String cursor;
private ScanStrategy<T> strategy;

私たちのステージはすべて、イテレーターのイテレーター固有の機能を定義するために設定されています。 そのためには、RedisIteratorクラスがIteratorインターフェイスを実装する必要があります。

public class RedisIterator<T> implements Iterator<List<T>> {
}

当然、 Iteratorインターフェースから継承されたhasNext()および next()メソッドをオーバーライドする必要があります。

最初に、基礎となるロジックが単純であるため、簡単な成果である hasNext()メソッドを選択しましょう。 カーソル値が「0」になるとすぐに、スキャンが完了したことがわかります。 それでは、これを1行で実装する方法を見てみましょう。

@Override
public boolean hasNext() {
    return !"0".equals(cursor);
}

次に、スキャンの重労働を行う next()メソッドに取り組みましょう。

@Override
public List next() {
    if (cursor == null) {
        cursor = "0";
    }
    try (Jedis jedis = jedisPool.getResource()) {
        ScanResult scanResult = strategy.scan(jedis, cursor, scanParams);
        cursor = scanResult.getCursor();
        return scanResult.getResult();
    } catch (Exception ex) {
        log.error("Exception caught in next()", ex);
    }
    return new LinkedList();
}

ScanResultは、スキャンされた結果だけでなく、後続のスキャンに必要な次のカーソル値も提供することに注意する必要があります。

最後に、この機能を有効にして、RedisClientクラスにRedisIteratorを作成できます。

public RedisIterator iterator(int initialScanCount, String pattern, ScanStrategy strategy) {
    return new RedisIterator(jedisPool, initialScanCount, pattern, strategy);
}

7.3. Redisイテレータで読む

Iterator インターフェースを使用してRedisイテレーターを設計したため、hasNext()がtrueを返す限り、next()メソッドを使用してコレクション値を読み取るのは非常に直感的です。

完全性と単純さのために、最初にスポーツボールに関連するデータセットをRedisハッシュに保存します。 その後、 RedisClient を使用して、Hscanスキャン戦略を使用してイテレーターを作成します。 これが実際に動作するのを見て、実装をテストしてみましょう。

@Test
public void testHscanStrategy() {
    HashMap<String, String> hash = new HashMap<String, String>();
    hash.put("cricket", "160");
    hash.put("football", "450");
    hash.put("volleyball", "270");
    redisClient.hmset("balls", hash);

    Hscan scanStrategy = new Hscan("balls");
    int iterationCount = 2;
    RedisIterator iterator = redisClient.iterator(iterationCount, "*", scanStrategy);
    List<Map.Entry<String, String>> results = new LinkedList<Map.Entry<String, String>>();
    while (iterator.hasNext()) {
        results.addAll(iterator.next());
    }
    Assert.assertEquals(hash.size(), results.size());
}

さまざまなタイプのコレクションで使用可能なキーをスキャンして読み取るための残りの戦略をテストおよび実装するために、ほとんど変更を加えずに同じ思考プロセスに従うことができます。

8. 結論

このチュートリアルは、Redisで一致するすべてのキーを読み取る方法を学ぶことを目的として開始されました。

一度にキーを読み取るためにRedisが提供する簡単な方法があることがわかりました。 単純ですが、これがどのようにリソースに負担をかけ、したがって本番システムには適さないかについて説明しました。 さらに深く掘り下げると、読み取りクエリのRedisキーを照合することで、イテレータベースのスキャンアプローチがあることがわかりました。

いつものように、この記事で使用されているJava実装の完全なソースコードは、GitHubからで入手できます。