1. 概要

この記事は、 Jedis の紹介です。これは、 Redis 用のJavaのクライアントライブラリです。これは、ディスク上でも保持できる人気のあるメモリ内データ構造ストアです。 キーストアベースのデータ構造によって駆動され、データを永続化し、データベース、キャッシュ、メッセージブローカーなどとして使用できます。

まず、ジェダイがどのような状況で役立つのか、そしてそれが何であるのかを説明します。

以降のセクションでは、さまざまなデータ構造について詳しく説明し、トランザクション、パイプライン、およびパブリッシュ/サブスクライブ機能について説明します。 最後に、接続プールとRedisクラスターを使用します。

2. なぜジェダイ?

Redisは、公式サイトに最もよく知られているクライアントライブラリをリストしています。 ジェダイには複数の選択肢がありますが、現在、推奨スターに値するのはレタスリディソンの2つだけです。

これらの2つのクライアントには、スレッドセーフ、透過的な再接続処理、非同期APIなどの独自の機能がありますが、これらすべての機能にはJedisにはありません。

ただし、他の2つよりも小さく、かなり高速です。 さらに、Spring Framework開発者が選択するクライアントライブラリであり、3つすべての中で最大のコミュニティがあります。

3. Mavenの依存関係

pom.xmlで必要な唯一の依存関係を宣言することから始めましょう。

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

ライブラリの最新バージョンをお探しの場合は、このページをご覧ください。

4. Redisのインストール

最新バージョンのRedisの1つをインストールして起動する必要があります。 現在、最新の安定バージョン(3.2.1)を実行していますが、3.x以降のバージョンは問題ありません。

LinuxおよびMacintosh用のRedisの詳細については、こちらをご覧ください。基本的なインストール手順は非常に似ています。 Windowsは公式にはサポートされていませんが、このポートは適切に保守されています。

その後、Javaコードから直接飛び込んで接続できます。

Jedis jedis = new Jedis();

デフォルト以外のポートまたはリモートマシンでサービスを開始していない限り、デフォルトコンストラクターは正常に機能します。この場合、コンストラクターにパラメーターとして正しい値を渡すことで、サービスを正しく構成できます。

5. Redisデータ構造

ほとんどのネイティブ操作コマンドがサポートされており、便利なことに、通常は同じメソッド名を共有しています。

5.1. 文字列

文字列は最も基本的な種類のRedis値であり、単純なKey-Valueデータ型を永続化する必要がある場合に役立ちます。

jedis.set("events/city/rome", "32,15,223,828");
String cachedResponse = jedis.get("events/city/rome");

変数cachedResponseは、値32,15,223,828を保持します。 後で説明する有効期限のサポートと組み合わせると、Webアプリケーションやその他のキャッシュ要件で受信したHTTP要求に対して、非常に高速で簡単に使用できるキャッシュレイヤーとして機能します。

5.2. リスト

Redisリストは、挿入順序でソートされた単純な文字列のリストであり、たとえばメッセージキューを実装するための理想的なツールになります。

jedis.lpush("queue#tasks", "firstTask");
jedis.lpush("queue#tasks", "secondTask");

String task = jedis.rpop("queue#tasks");

変数taskは、値firstTaskを保持します。 任意のオブジェクトをシリアル化して文字列として永続化できるため、キュー内のメッセージは必要に応じてより複雑なデータを伝送できることに注意してください。

5.3. セット

Redisセットは、繰り返されるメンバーを除外する場合に便利な、順序付けされていない文字列のコレクションです。

jedis.sadd("nicknames", "nickname#1");
jedis.sadd("nicknames", "nickname#2");
jedis.sadd("nicknames", "nickname#1");

Set<String> nicknames = jedis.smembers("nicknames");
boolean exists = jedis.sismember("nicknames", "nickname#1");

Javaセットニックネームのサイズは2になり、ニックネーム#1の2番目の追加は無視されました。 また、examples変数の値はtrueになります。メソッドsismemberを使用すると、特定のメンバーの存在をすばやく確認できます。

5.4. ハッシュ

Redisハッシュは、StringフィールドとString値の間のマッピングです。

jedis.hset("user#1", "name", "Peter");
jedis.hset("user#1", "job", "politician");
		
String name = jedis.hget("user#1", "name");
		
Map<String, String> fields = jedis.hgetAll("user#1");
String job = fields.get("job");

ご覧のとおり、オブジェクト全体を取得する必要がないため、オブジェクトのプロパティに個別にアクセスする場合、ハッシュは非常に便利なデータ型です。

5.5. ソートされたセット

並べ替えられたセットは、各メンバーに関連付けられたランキングがあり、それらを並べ替えるために使用されるセットのようなものです。

Map<String, Double> scores = new HashMap<>();

scores.put("PlayerOne", 3000.0);
scores.put("PlayerTwo", 1500.0);
scores.put("PlayerThree", 8200.0);

scores.entrySet().forEach(playerScore -> {
    jedis.zadd(key, playerScore.getValue(), playerScore.getKey());
});
		
String player = jedis.zrevrange("ranking", 0, 1).iterator().next();
long rank = jedis.zrevrank("ranking", "PlayerOne");

変数playerは、値 PlayerThree を保持します。これは、上位1人のプレーヤーを取得しており、彼が最高スコアのプレーヤーであるためです。 rank 変数の値は1になります。これは、 PlayerOne がランキングの2番目であり、ランキングがゼロベースであるためです。

6. トランザクション

トランザクションは、アトミック性とスレッドセーフ操作を保証します。つまり、他のクライアントからのリクエストがRedisトランザクション中に同時に処理されることはありません。

String friendsPrefix = "friends#";
String userOneId = "4352523";
String userTwoId = "5552321";

Transaction t = jedis.multi();
t.sadd(friendsPrefix + userOneId, userTwoId);
t.sadd(friendsPrefix + userTwoId, userOneId);
t.exec();

Transaction をインスタンス化する直前に特定のキーを「監視」することで、特定のキーに依存してトランザクションを成功させることもできます。

jedis.watch("friends#deleted#" + userOneId);

トランザクションが実行される前にそのキーの値が変更された場合、トランザクションは正常に完了しません。

7. パイプライン

複数のコマンドを送信する必要がある場合、それらを1つのリクエストにまとめて、パイプラインを使用することで接続オーバーヘッドを節約できます。これは本質的にネットワークの最適化です。 操作が相互に独立している限り、この手法を利用できます。

String userOneId = "4352523";
String userTwoId = "4849888";

Pipeline p = jedis.pipelined();
p.sadd("searched#" + userOneId, "paris");
p.zadd("ranking", 126, userOneId);
p.zadd("ranking", 325, userTwoId);
Response<Boolean> pipeExists = p.sismember("searched#" + userOneId, "paris");
Response<Set<String>> pipeRanking = p.zrange("ranking", 0, -1);
p.sync();

String exists = pipeExists.get();
Set<String> ranking = pipeRanking.get();

コマンド応答に直接アクセスできないことに注意してください。代わりに、パイプラインが同期された後に基になる応答を要求できるResponseインスタンスが与えられます。

8. パブリッシュ/サブスクライブ

Redisメッセージングブローカー機能を使用して、システムのさまざまなコンポーネント間でメッセージを送信できます。 サブスクライバースレッドとパブリッシャースレッドが同じJedis接続を共有していないことを確認してください。

8.1. サブスクライバー

チャンネルに送信されたメッセージを購読して聞く:

Jedis jSubscriber = new Jedis();
jSubscriber.subscribe(new JedisPubSub() {
    @Override
    public void onMessage(String channel, String message) {
        // handle message
    }
}, "channel");

サブスクライブはブロック方法です。JedisPubSubから明示的にサブスクライブを解除する必要があります。 onMessage メソッドをオーバーライドしましたが、オーバーライドできる便利なメソッドは他にもたくさんあります。

8.2. 出版社

次に、発行者のスレッドから同じチャネルにメッセージを送信するだけです。

Jedis jPublisher = new Jedis();
jPublisher.publish("channel", "test message");

9. 接続プール

私たちがジェダイのインスタンスを扱ってきた方法は素朴であることを知っておくことが重要です。 実際のシナリオでは、単一のインスタンスはスレッドセーフではないため、マルチスレッド環境で単一のインスタンスを使用することは望ましくありません。

幸いなことに、Redisへの接続プールを簡単に作成して、オンデマンドで再利用できます。このプールは、使い終わったときにリソースをプールに戻す限り、スレッドセーフで信頼性があります。

JedisPoolを作成しましょう。

final JedisPoolConfig poolConfig = buildPoolConfig();
JedisPool jedisPool = new JedisPool(poolConfig, "localhost");

private JedisPoolConfig buildPoolConfig() {
    final JedisPoolConfig poolConfig = new JedisPoolConfig();
    poolConfig.setMaxTotal(128);
    poolConfig.setMaxIdle(128);
    poolConfig.setMinIdle(16);
    poolConfig.setTestOnBorrow(true);
    poolConfig.setTestOnReturn(true);
    poolConfig.setTestWhileIdle(true);
    poolConfig.setMinEvictableIdleTimeMillis(Duration.ofSeconds(60).toMillis());
    poolConfig.setTimeBetweenEvictionRunsMillis(Duration.ofSeconds(30).toMillis());
    poolConfig.setNumTestsPerEvictionRun(3);
    poolConfig.setBlockWhenExhausted(true);
    return poolConfig;
}

プールインスタンスはスレッドセーフであるため、静的にどこかに保存できますが、アプリケーションのシャットダウン時にリークが発生しないように、プールの破棄に注意する必要があります。

これで、必要に応じて、アプリケーションのどこからでもプールを利用できるようになりました。

try (Jedis jedis = jedisPool.getResource()) {
    // do operations with jedis resource
}

Jedisリソースを手動で閉じる必要がないように、Java try-with-resourcesステートメントを使用しましたが、このステートメントを使用できない場合は、finally句でリソースを手動で閉じることもできます。

厄介なマルチスレッドの問題に直面したくない場合は、アプリケーションで説明したようなプールを使用してください。 明らかに、プール構成パラメーターを試して、システムの最適なセットアップに適合させることができます。

10. Redisクラスター

このRedisの実装は、簡単なスケーラビリティと高可用性を提供します。慣れていない場合は、公式仕様を読むことをお勧めします。 Redisクラスターのセットアップについては、この記事の範囲から少し外れているため、ここでは説明しませんが、ドキュメントが完成したら、問題なく実行できます。

準備ができたら、アプリケーションから使用を開始できます。

try (JedisCluster jedisCluster = new JedisCluster(new HostAndPort("localhost", 6379))) {
    // use the jedisCluster resource as if it was a normal Jedis resource
} catch (IOException e) {}

マスターインスタンスの1つからホストとポートの詳細を提供するだけで、クラスター内の残りのインスタンスが自動検出されます。

これは確かに非常に強力な機能ですが、特効薬ではありません。 Redis Clusterを使用する場合、トランザクションを実行したり、パイプラインを使用したりすることはできません。これは、多くのアプリケーションがデータの整合性を確保するために依存する2つの重要な機能です。

クラスタ化された環境では、キーが複数のインスタンス間で永続化されるため、トランザクションは無効になります。 異なるインスタンスでのコマンド実行を伴う操作では、操作のアトミック性とスレッドセーフは保証されません。

いくつかの高度なキー作成戦略により、同じインスタンスで永続化するのに興味深いデータがそのように永続化されることが保証されます。 理論的には、これにより、Redisクラスターの基盤となるJedisインスタンスの1つを使用してトランザクションを正常に実行できるようになります。

残念ながら、現在、特定のキーがJedis(実際にはRedisによってネイティブにサポートされている)を使用して保存されているRedisインスタンスを見つけることができないため、トランザクション操作を実行する必要があるインスタンスがわかりません。 これに興味がある場合は、ここで詳細情報を見つけることができます。

11. 結論

Redisの機能の大部分はすでにJedisで利用可能であり、その開発は順調に進んでいます。

強力なメモリ内ストレージエンジンをアプリケーションに統合する機能をほとんど手間をかけずに提供します。スレッドセーフの問題を回避するために接続プールを設定することを忘れないでください。

コードサンプルはGitHubプロジェクトにあります。