バックエンドに Redis を置いた、 Spring Boot アプリケーションを作りました。
そのときに調べた実装方法のメモです。

Redis に格納するデータフォーマットは

  • 文字列
  • Java シリアライズ
  • JSON

をまとめました。

Redis とは

  • Key-Value ストア (KVS)
  • インメモリ DB なので高速
  • データに型がある (文字列, リスト, セット, ソート済セット, ハッシュ)

環境

  • CentOS 6.6
  • Redis 2.8.19
  • Java 1.8.0 update 51
  • Maven 3.3.3
  • Spring Boot 1.2.5
  • Lombok 1.16.4 (Getter/Setter 作成に使ってます, 本題ではないです)

依存関係

pom.xmldependencies
spring-boot-starter-redis を追加します。

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-redis</artifactId>
</dependency>

接続設定

Redis サーバへ接続するための設定です。

ここでは application.yml に書きました。
.properties にするとか、 Java 実行パラメータ (-D) で渡すとかはお好みで。

spring.redis:
  host: localhost
  port: 6379
  password: null
  database: 0
  pool:
    max-idle: 8
    min-idle: 0
    max-active: 8
    max-wait: -1

ちなみに上記はデフォルト値。
この値のまま使うなら敢えて書く必要はないです (^^;

spring.redis.database

Redis ではデータベースを複数持つことが出来るようで、
0 から順に番号付けされてるようです。
この設定値には、どのデータベースに繋ぐかの番号を指定します。

僕はテスト時は spring.redis.database=1 にして、
アプリケーション実行に使うエリアと、
テストコードが使うエリアとに分けて利用してます :)

spring.redis.pool.*

デフォルトでは Apache Commons Pool によって
コネクションをプールして使いまわすっぽいです。
どれくらいプールするかの設定をこのプロパティでいじれるようになってました。

RedisTemplate について

Java から Redisアクセスするには、 RedisTemplate 系のクラスを使います。
Spring Boot の自動設定では、 StringRedisTemplate が DI コンテナに登録されてました。

文字列で保存/取得してみる

REST コントローラで、 Redis へ PUT/GET してみます。

Redis アクセスには StringRedisTemplate を使います。
データ型には、文字列, リスト, ハッシュを使ってみました。

@Data
public class Hoge {
    private String string;
    private List<String> list;
    private Map<String, String> map;
}
@RestController @RequestMapping(value = "/hoge-string")
public class HogeStringController {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @RequestMapping(method = RequestMethod.PUT)
    public void put(@RequestBody Hoge value) throws Exception {
        redisTemplate.opsForValue()
            .set("hoge-string:string", value.getString());
        redisTemplate.delete("hoge-string:list");
        redisTemplate.opsForList()
            .rightPushAll("hoge-string:list", value.getList().toArray(new String[0]));
        redisTemplate.delete("hoge-string:map");
        redisTemplate.opsForHash()
            .putAll("hoge-string:map", value.getMap());
    }

    @RequestMapping(method = RequestMethod.GET)
    public Hoge get() throws Exception {
        Hoge hoge = new Hoge();
        hoge.setString(
            redisTemplate.opsForValue().get("hoge-string:string")
        );
        hoge.setList(
            redisTemplate.opsForList().range("hoge-string:list", 0, -1)
        );
        hoge.setMap(
            redisTemplate.<String, String>opsForHash().entries("hoge-string:map")
        );
        return hoge;
    }

}

実行すると。

$ curl -X PUT 'http://localhost:8080/hoge-string' \
  -H 'Content-Type:application/json' \
  -d '{
    "string": "hoge",
    "list": [
      "hoge",
      "fuga"
    ],
    "map": {
      "hoge": "fuga",
      "fuga": "piyo"
    }
  }'

$ curl 'http://localhost:8080/hoge-string'
{"string":"hoge","list":["hoge","fuga"],"map":{"fuga":"piyo","hoge":"fuga"}}

Redis の登録内容は。

$ redis-cli

> get hoge-string:string
"hoge"

> lrange hoge-string:list 0 -1
1) "hoge"
2) "fuga"

> hgetall hoge-string:map
1) "hoge"
2) "fuga"
3) "fuga"
4) "piyo"

Java のシリアライズで保存/取得してみる

さっきの Hoge クラスを Java のシリアライズでバイナリにして、
1件の Key-Value として登録します。

まず自前の RedisTemplate を作って、 DI コンテナに登録してやります。
このとき、シリアライザに JdkSerializationRedisSerializer をセットします。
キーまでバイナリになると確認しにくいので、
キーのシリアライザには StringRedisSerializer をセットしました。

用意されてるシリアライザを組み合わせるだけなので楽ちんです。

@Configuration
public class RedisConfiguration {
    @Bean
    public RedisTemplate<String, Hoge> serialRedisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Hoge> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
        redisTemplate.setHashKeySerializer(redisTemplate.getKeySerializer());
        redisTemplate.setHashValueSerializer(redisTemplate.getValueSerializer());
        return redisTemplate;
    }
}

Hoge クラスは Serializable にしておきます。

import java.io.Serializable;

public class Hoge implements Serializable {
    // ...
}

さっきとは別の REST コントローラを作って、 Redis へ PUT/GET してみます。
RedisTemplate は新たに作ったものをインジェクション。
Hoge クラスを丸っと Redis へ投げてます。

@RestController @RequestMapping(value = "/hoge-serial")
public class HogeSerialController {

    @Autowired @Qualifier("serialRedisTemplate")
    private RedisTemplate<String, Hoge> redisTemplate;

    @RequestMapping(method = RequestMethod.PUT)
    public void put(@RequestBody Hoge value) throws Exception {
        redisTemplate.opsForValue().set("hoge-serial", value);
    }

    @RequestMapping(method = RequestMethod.GET)
    public Hoge get() throws Exception {
        return redisTemplate.opsForValue().get("hoge-serial");
    }

}

実行すると。

$ curl -X PUT 'http://localhost:8080/hoge-serial' \
  -H 'Content-Type:application/json' \
  -d '{
    "string": "hoge",
    "list": [
      "hoge",
      "fuga"
    ],
    "map": {
      "hoge": "fuga",
      "fuga": "piyo"
    }
  }'

$ curl 'http://localhost:8080/hoge-serial'
{"string":"hoge","list":["hoge","fuga"],"map":{"hoge":"fuga","fuga":"piyo"}}

Redis の登録内容は。

$ redis-cli

> get hoge-serial
"\xac\xed\x00\x05sr\x00#akihyro.tryspringbootwithredis.Hoge\xa2[\xd9\xb5?\xa0\xcb=\x02\x00\x03L\x00\x04listt\x00\x10Ljava/util/List;L\x00\x03mapt\x00\x0fLjava/util/Map;L\x00\x06stringt\x00\x12Ljava/lang/String;xpsr\x00\x13java.util.ArrayListx\x81\xd2\x1d\x99\xc7a\x9d\x03\x00\x01I\x00\x04sizexp\x00\x00\x00\x02w\x04\x00\x00\x00\x02t\x00\x04hoget\x00\x04fugaxsr\x00\x17java.util.LinkedHashMap4\xc0N\\\x10l\xc0\xfb\x02\x00\x01Z\x00\x0baccessOrderxr\x00\x11java.util.HashMap\x05\a\xda\xc1\xc3\x16`\xd1\x03\x00\x02F\x00\nloadFactorI\x00\tthresholdxp?@\x00\x00\x00\x00\x00\x0cw\b\x00\x00\x00\x10\x00\x00\x00\x02t\x00\x04hoget\x00\x04fugat\x00\x04fugat\x00\x04piyox\x00t\x00\x04hoge"

…やはりバイナリは見にくい (^^;

JSON で保存/取得してみる

さっきの Hoge クラスを JSON にして、1件の Key-Value として登録します。

まずは JSON 版の RedisTemplate を DI コンテナに登録。
シリアライザには Jackson2JsonRedisSerializer をセットします。

@Configuration
public class RedisConfiguration {

    // ...

    @Bean
    public RedisTemplate<String, Hoge> jsonRedisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Hoge> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Hoge.class));
        redisTemplate.setHashKeySerializer(redisTemplate.getKeySerializer());
        redisTemplate.setHashValueSerializer(redisTemplate.getValueSerializer());
        return redisTemplate;
    }

}

また新しい REST コントローラを作って、 Redis へ PUT/GET してみます。

@RestController @RequestMapping(value = "/hoge-json")
public class HogeJsonController {

    @Autowired @Qualifier("jsonRedisTemplate")
    private RedisTemplate<String, Hoge> redisTemplate;

    @RequestMapping(method = RequestMethod.PUT)
    public void put(@RequestBody Hoge value) throws Exception {
        redisTemplate.opsForValue().set("hoge-json", value);
    }

    @RequestMapping(method = RequestMethod.GET)
    public Hoge get() throws Exception {
        return redisTemplate.opsForValue().get("hoge-json");
    }

}

実行すると。

$ curl -X PUT 'http://localhost:8080/hoge-json' \
  -H 'Content-Type:application/json' \
  -d '{
    "string": "hoge",
    "list": [
      "hoge",
      "fuga"
    ],
    "map": {
      "hoge": "fuga",
      "fuga": "piyo"
    }
  }'

$ curl 'http://localhost:8080/hoge-json'
{"string":"hoge","list":["hoge","fuga"],"map":{"hoge":"fuga","fuga":"piyo"}}

Redis の登録内容は。

$ redis-cli

> get hoge-json
"{\"string\":\"hoge\",\"list\":[\"hoge\",\"fuga\"],\"map\":{\"hoge\":\"fuga\",\"fuga\":\"piyo\"}}"

Redis に格納するフォーマットで好きなもの

アプリの特性にもよるだろうけど、僕は今のところ JSON で格納が好きです。

  • リストやハッシュの構造を持てる。
  • 値を人間が確認できる。
    • redis-cli だけでは見にくいけど…
    • Redis Desktop Manager を使うと見やすい。
  • 項目を追加しやすい。
    • 前記のコードで言うと、 Hoge クラスにフィールドを追加しやすい。
    • JdkSerializationRedisSerializer では、
      フィールド追加前に保存したエントリを読み込もうとすると
      SerializationException が発生する。
    • Jackson2JsonRedisSerializer では、
      新たに追加したフィールドは null で返してくれる。

おまけ: クライアントは Redis Desktop Manager が便利

Redis Desktop Manager
http://redisdesktop.com

GUI でコロン区切りでいい感じにツリー構造にしてくれるのと、
保存形式が JSON なら見やすい形にも出来ました :)

Image: Desktop Manager

おまけ: AWS では ElastiCache が便利

もしアプリケーションを AWS で動かすなら、
Amazon ElastiCache の Redis が便利でした。

キャッシュノードを複数の AZ に立てれば、レプリケーショングループのオプション1つで
マルチ AZ + レプリケーションしてくれて、 SPOF を回避できます。

最初、 memcached と Redis どちらを使うか悩みましたが、
この楽ちんさで Redis を採用しました :)

コード (GitHub)

akihyro/try-spring-boot-with-redis - GitHub

参考