📕

RedisのJSON型とJava / Springでの使用例

2024/04/03に公開

Redisを効果的に使うためのJSON型と、Java / Spring Bootでの使用例を紹介します。

Redis JSON型について

RedisのJSON型は、単純にJSON形式の文字列をRedisに格納するだけではなく、JSONドキュメントを効率的に処理するための手段を提供するものです。MySQLやPostgreSQLにも、文字列型と別にjson型がありますが、それと同じようにString型とは一線を画すものになっています。

Redisのデータ型

まずはじめに、Redisのデータ型については以下のページにまとまっています。

https://redis.io/docs/data-types/

主に使われるデータ型(基本データ型)は以下の5つです。

  • String型(文字列)
  • Hash型(ハッシュ)
  • List型(リスト)
  • Set型(セット)
  • SortedSet型(ソート済みセット)

データ型としては他にも、地理空間(Geospatial)やBitMap等があります。ただ、これらの実体はそれぞれSortedSetやString型ですので、基本データ型をラップした補助的な型、という風に私は捉えています。

今回紹介するJSON型も、Redisがサポートするデータ型の1つです。JSON型は、上記の5つの基本データ型とは別の、独自の型という位置づけになります。同じような専用の型としては、他にストリームがあります。

https://redis.io/docs/data-types/json/

JSON型は、モジュールを通じてRedisの拡張としてインストールすることができ、JSON形式のデータの取り扱いに特化した型です。
JSONドキュメントをそのままの形で保存し、それぞれの項目に対して操作を実行することができます。

以下はコマンドでの簡単な例です。Redisでは TYPE コマンドで型を調べることができますが、 JSON型の場合は ReJSON-RL が返ってきます。このことからも、専用の型であることが分かりますね。

redis> JSON.SET k1 . '{"name":"taro", "age":20}'
OK
redis> JSON.GET k1 .name
"\"taro\""
redis> TYPE k1
ReJSON-RL

JSON型のメリット

単純にJSON文字列をRedisで扱いたいだけであれば、基本データ型(例えばString型)を利用して、JSONシリアライズした文字列を格納するだけでも扱えますが、JSON型には、以下のようなメリットがあります。

1. 計算リソースの節約

JSON型では特定の属性だけを更新したり取得したりすることができます。
Redisを使うシーンでは、パフォーマンスや効率性が気になるが多いので、JSONのような構造化データを扱う場合に、ネットワークオーバーヘッドや計算時間を節約することができるのはメリットになります。

2. 複数のクライアントアプリケーションから同一キーへの書き込みがある場合に、データが失われる危険性を抑えることができる

データ全体をJSONシリアライズしてSET, GETする方法は、1つのアンチパターンとして紹介されています。複数のクライアントアプリケーションから書き込みがあった時に、データが上書きされ、一部のデータが失われる危険性があるためです。まぁ、そういう作りのアプリケーションでなければ問題にはならないのですが…。

https://redis.com/glossary/json-storage/
(日本語訳) https://zenn.dev/tk42/books/adbf4f87beed12/viewer/69b4d0

Worse yet, many applications would just GET the entire JSON string, deserialize it, manipulate it, re-serialize and SET it again at the application. This is an anti-pattern. There is a very real risk of losing data with this method

JSON型では前述の通り、特定の属性だけを更新することができるので、各クライアントが自分の責任の持つ属性だけを更新することにより、データが失われる危険性を抑えることができます。

3. JSON.MGETによる複数キー取得のサポート

1つ目と2つ目のメリットは、JSON型のメリットとして謳われている点ですが、私たちがJSON型に着目したポイントはもう1つあります。JSON.MGET コマンドによって複数キー取得がサポートされていることです。

https://redis.io/commands/json.mget/

Redisがいくら高速と言っても、アクセスに通信が発生することには違いがないので、可能な限りアクセス頻度を少なくすることに越したことはありません。
String型の MGET でも複数キー取得ができますが、文字列全体を取得することしかできないので、柔軟性に欠けます。
JSON.MGET を使うと、階層構造や配列を含んだJSONデータが格納されたRedisの空間から、必要な属性だけを一括で取得することができます。

JSON型の使い方

JSON型を扱うためのRedisのコマンドは JSON. で始まります。
全部で20個以上あるのですが、基本的な操作で使うのは JSON.SETJSON.GETの2つかと思いますので、ここから慣れていくとよいでしょう。

JSON型を使う上で、JSONパスの理解は不可欠です。 JSON.SET, JSON.GET だけでもある程度の勘所をつかむことができます。詳しくはドキュメントを参照してみてください。

https://redis.io/docs/data-types/json/path/

ローカル(Docker)での利用

私たちはローカルの開発環境構築にDocker + Docker Composeを使っていますので、DockerにおけるJSON型の導入方法を紹介します。

JSON型のサポートはRedisの拡張モジュールになっているため、素のRedisサーバでは利用できません。
追加インストールする方法もありますが、Dockerを使っているのであれば Redis Stackを指定する方法が簡単です。Redis Stackは、RedisJSONを含む複数のモジュールを同梱した拡張パックです。

Dockerfile
- FROM redis:latest
+ FROM redis/redis-stack:latest

EXPOSE 6379

ちなみに redislabs/rejson というDockerイメージは現在非推奨になっており、現在はRedis Stackの利用が推奨されています。

AWSでの利用

クラウド上の使用方法として、AWSの場合を紹介します。
AWSはRedisのマネージドサービスとしてAmazon ElastiCache for Redisを提供しています。

前の項で触れた通り、JSONサポートはRedisのモジュール(拡張)ですが、ElastiCache for Redisでは、JSON型が標準サポートされていますので、特別な設定なく使うことができます。

https://docs.aws.amazon.com/ja_jp/AmazonElastiCache/latest/red-ug/json-gs.html

Javaアプリケーションからの利用

実際のサービスでは、コマンドではなくアプリケーションコードからJSON型を操作することになるので、ここからは、私たちが利用しているJavaとSpring Bootを使ったアプリケーションでの使用例を紹介します。

JSON型を使う方法は色々あります。以下は一例です。

特に優劣があるわけではないので、合ったものを選びましょう。
今回は、Redis OM SpringとRedisTemplateをメインに紹介します。

Redis OM Spring

https://redis.io/docs/connect/clients/om-clients/stack-spring/

Redis OM Springは、「Redis OM (Object-Mapper libraries for Redis Stack)」という、Redisに対するオブジェクトマッピングのライブラリ群の中の、Spring / Java向けの実装という位置づけになっています。
Spring / Java以外には、Node.js, Python, C# / .NET版もあるようです。

https://redis.io/docs/connect/clients/om-clients/

OM(Object-Mapper)の名前の通り、JSON型とJavaオブジェクトのマッピングに特化しています。
ただ、Redis OM自体が(Beta)であり、Redis OM SpringもまだProduction Readyな状態ではないことには留意しましょう( https://github.com/redis/redis-om-spring/wiki/Project-Stages )。
また、執筆時点でのJavaの要求バージョンはJava 17以上なので、正式リリースされた時に使う場合にも、アプリケーション側がJava 17以上の対応が必要になると思います。

RedisTemplate

RedisTemplateを使う場合、opsFor* メソッド(例: opsForValue 等)によって、各データ型に応じたオペレーションを取得して操作します。
ただし、JSON型のための opsForJson 的なメソッドは提供されていませんので、一番原始的な execute を使うことにします。

JSON.SETJSON.GET の単純なコマンドでの使用例です。

JSON.SET
// JSON.SET <key> <path> <json> [NX | XX]
// JSON.SET k1 . '{"a":{"a":1, "b":2, "c":3}}'
String key = "k1";
String path = ".";
String json = "{\"a\":{\"a\":1, \"b\":2, \"c\":3}}";
redisTemplate.execute((RedisCallback<byte[]>) c ->
        (byte[]) c.execute("JSON.SET",
                key.getBytes(StandardCharsets.UTF_8),
                path.getBytes(StandardCharsets.UTF_8),
                json.getBytes(StandardCharsets.UTF_8)
        ));
JSON.GET
// JSON.GET <key> [INDENT indentation-string] [NEWLINE newline-string] [SPACE space-string] [NOESCAPE] [path ...]
// JSON.GET k1 .a.c
String key = "k1";
String path = ".a.c";
byte[][] args = {
    key.getBytes(StandardCharsets.UTF_8),
    path.getBytes(StandardCharsets.UTF_8)
};

Object result = redisTemplate.execute((RedisCallback<Object>) c ->
        c.execute("JSON.GET", args));
if (!(result instanceof byte[])) {
    throw new Exception();
}

String value = new String((byte[]) result, StandardCharsets.UTF_8); // "3"

簡単な実行コマンドでも、かなり泥臭いコードになってしまいました。
RedisTemplateを使うことで、コネクション周りなどの面倒はフレームワークに任せることができますが、データに対する操作は、実装側でうまく付き合っていく必要がありそうです。

気になる場合は、前述のLettuceModのように、抽象度の高いライブラリの利用を検討してみるとよいでしょう。

おわりに

RedisもJSONもJavaも定番の技術だと思いますが、それらの組み合わせ方については、あまり情報が見当たらなかったので、今回、調べたことをまとめてみました。

キャッシュは奥が深いテーマです。Redisに依存した機能を使ってゴリゴリにチューニングするのも1つの戦略ですが、シンプルさという意味では、RedisTemplateにJSON用のシリアライザ(Jackson2JsonRedisSerializerやGenericJackson2JsonRedisSerializer等)を使って、基本データ型で運用するだけで十分、というケースが多いのかもしれないですね。

Cariot開発チーム(フレクト)

Discussion