Spring BootでSpring Data Redisを試す

Spring BootでSpring Data Redisを試したメモです。

Spring BootでSpring Data Redisを試してみました。

Spring Data Redis

前回Redisをredis-clijedisなどで操作してきましたが、Spring Data Redisから触ってみます。
Spring Data Redisではjedisやlettuceなどの低レイヤーなAPIを直接操作しなくていいように抽象化されています。
SpringのCache Abstractionやtransactionが使えたりと色々便利です。

Spring Data Redis

Redis4.0.1で試してみます。

dependency

spring-boot-starter-parentの2系はまだGAになってなかったので、
現時点で最新の1.5.8.RELEASEを指定しました。
(依存関係のspring-data-redisは1.8.8です)

dependencyにはspring-boot-starter-webとspring-boot-starter-data-redisを追加しました。
テスト用のモデルのためにlombokも追加しました。

pom.xml

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.8.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.16.18</version>
        <scope>provided</scope>
    </dependency>

    <!-- test -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.assertj</groupId>
        <artifactId>assertj-core</artifactId>
        <version>3.8.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

接続確認

とりあせず接続して簡単なset, getを試してみます。
自分の環境ではRedisがデフォルト(localhost, port:6379)で動いているので特に何も設定せずに接続できます。

別のサーバ、ポートで動いている場合はconf.propertiesに指定します。

conf.properties

spring.redis.host=xxxxxx
spring.redis.port=1234

下記のようなテストを記述。

@Autowired
private StringRedisTemplate redisTemplate; // (1)

@Test
public void basic() {
    redisTemplate.opsForValue().set("my_key", "my_val"); // (2)
    System.out.println(redisTemplate.opsForValue().get("my_key")); // (3)
}

(1) StringRedisTemplateをインジェクト。
RedisTemplateというRedisのデータを扱うためのヘルパークラスがあり、
どのようなキーと値を扱うか、どのようにシリアライズするかなどを指定出来ます。
StringRedisTemplateはStringでデータを扱う専用になっているクラスです(RedisではStringで扱うことが多いため)
(2) opsForValue()で(Redisの)String型用のオペレーションを取得してset()でキーと値を設定。
(3) 同様にget()で指定したキーの値を取得。

結果

my_val

各データ型

Redisの各データ型について、使いそうなメソッドだけ触ってみます。
Redisの各データ型に対して、それぞれXXXOperationsというインターフェースが定義されているので、
RedisTemplate(StringRedisTemplate)から取得して使用します。

Redisデータ型 Operations
String ValueOperations
List ListOperations
Set SetOperations
Hash HashOperations
ZSet ZSetOperations

各Operationsに関係なく共通なものはRedisTemplateにメソッドがあります。

RedisTemplate

各Operationsに関係ない共通なメソッドがあります。

オペレーションは頻繁に使うので下記のように変数に切り出しておきます。

ValueOperations<String, String> ops = redisTemplate.opsForValue();

delete()
delete()で指定したキーの値を削除。

ops.set("my_key", "my_val");
System.out.println(ops.get("my_key"));
redisTemplate.delete("my_key");
System.out.println(ops.get("my_key"));

結果

my_val
null

hasKey()
hasKey()で指定したキーが存在する場合true、存在しない場合false

Map<String, String> map = new HashMap<>();
map.put("AAA", "111");
map.put("BBB", "222");
map.put("CCC", "333");
ops.multiSet(map);
System.out.println(redisTemplate.hasKey("AAA"));
System.out.println(redisTemplate.hasKey("DDD"));

結果

true
false

expire()
expire()で対象キーの存続時間(秒)を指定できる。
存続時間を過ぎるとキーは自動的に削除される。

ops.set("my_key", "ABCDE");
redisTemplate.expire("my_key", 5, TimeUnit.SECONDS);
System.out.println(ops.get("my_key"));
System.out.println("Wait for 6 sec...");
Thread.sleep(6_000L);
System.out.println(ops.get("my_key"));

結果

ABCDE
Wait for 6 sec...
null
ValueOperations

RedisのString型はValueOperationsを使用します。

ValueOperations<String, String> ops = redisTemplate.opsForValue();

set(), get()
set()で指定したキーに値を設定。
get()で指定したキーの値をフェッチ。

ops.set("my_key", "my_val");
System.out.println(ops.get("my_key"));

結果

my_val

setIfAbsent()
setIfAbsent()で存在しないキーの場合は設定される

ops.setIfAbsent("new_key", "new_val");
System.out.println(ops.get("new_key"));

結果

new_val

increment()
increment()で第2引数で指定した数だけ値をインクリメント、
マイナス値を指定するとデクリメント

ops.set("my_count", "100");
ops.increment("my_count", 10L);
System.out.println(ops.get("my_count"));

ops.increment("my_count", -10L);
System.out.println(ops.get("my_count"));

結果

110
100

multiSet(), multiGet()
multiSet()でキーと値のMapを指定することで複数の値を一度にセットできる。
multiGet()でキーのコレクションを指定すると複数の値を一度にフェッチできる。

Map<String, String> map = new HashMap<>();
map.put("AAA", "111");
map.put("BBB", "222");
map.put("CCC", "333");
ops.multiSet(map);
System.out.println(ops.multiGet(Arrays.asList("AAA", "BBB", "CCC")));

結果

[111, 222, 333]

set() with expire
set()に第3、4引数で存続時間を指定できる。
存続時間を過るとキーは自動的に削除される。

ops.set("my_key", "abcde", 5, TimeUnit.SECONDS);
System.out.println(ops.get("my_key"));
System.out.println("Wait for 6 sec...");
Thread.sleep(6_000L);
System.out.println(ops.get("my_key"));

結果

abcde
Wait for 6 sec...
null
ListOperations

RedisのList型はListOperationsを使用します。

ListOperations<String, String> ops = redisTemplate.opsForList();

rightPush()
rightPush()でListの末尾に追加

ops.rightPush("my_list", "aaa");
ops.rightPush("my_list", "bbb");
ops.rightPush("my_list", "ccc");
ops.rightPush("my_list", "ddd");
System.out.println(ops.range("my_list", 0, -1));

結果

[aaa, bbb, ccc, ddd]

rightPushAll()
rightPushAll()で複数の値を一度にrightPush可能

redisTemplate.delete("my_list");
ops.rightPushAll("my_list", "aaa", "bbb", "ccc", "ddd");
System.out.println(ops.range("my_list", 0, -1));

結果

[aaa, bbb, ccc, ddd]

要素のコレクションを指定することでも一度にrightPush可能

redisTemplate.delete("my_list");
List<String> names = Arrays.asList("aaa", "bbb", "ccc", "ddd");
ops.rightPushAll("my_list", names);
System.out.println(ops.range("my_list", 0, -1));

結果

[aaa, bbb, ccc, ddd]

index()
index()で指定したインデックスの要素を取得

System.out.println(ops.index("my_list", 0));
System.out.println(ops.index("my_list", 2));

結果

aaa
ccc

range()
range()で指定した範囲の要素を取得。
第2引数は開始するインデックス、第3引数は終了するインデックス。

System.out.println(ops.range("my_list", 0, 2));
System.out.println(ops.range("my_list", 1, 3));
System.out.println(ops.range("my_list", 0, -1));

結果

[aaa, bbb, ccc]
[bbb, ccc, ddd]
[aaa, bbb, ccc, ddd]

leftPush()
leftPush()でリストの先頭に指定された要素を挿入。

ops.leftPush("my_list", "aa");
System.out.println(ops.range("my_list", 0, -1));

結果

[aa, aaa, bbb, ccc, ddd]

leftPop(), rightPop()
leftPop()でリストの最初の要素を削除して取得。
rightPop()でリストの末尾の要素を削除して取得。

System.out.println(ops.range("my_list", 0, -1));
System.out.println(ops.leftPop("my_list"));
System.out.println(ops.range("my_list", 0, -1));
System.out.println(ops.rightPop("my_list"));
System.out.println(ops.range("my_list", 0, -1));

結果

[aa, aaa, bbb, ccc, ddd]
aa
[aaa, bbb, ccc, ddd]
ddd
[aaa, bbb, ccc]

trim()
trim()で指定した範囲を残すようにリストをトリムする。
第2引数は開始するインデックス、第3引数は終了するインデックス。

redisTemplate.delete("my_list");
ops.rightPushAll("my_list", "aaa", "bbb", "ccc", "ddd");
System.out.println(ops.range("my_list", 0, -1));
ops.trim("my_list", 0, 1);
System.out.println(ops.range("my_list", 0, -1));

結果

[aaa, bbb, ccc, ddd]
[aaa, bbb]
SetOperations

RedisのSet型はSetOperationsを使用します。

SetOperations<String, String> ops = redisTemplate.opsForSet();

add(), members()
add()でセットに要素を追加。
members()でセットに含まれるすべての要素を取得。

ops.add("my_set", "AAA");
ops.add("my_set", "BBB");
ops.add("my_set", "CCC");
ops.add("my_set", "AAA");
System.out.println(ops.members("my_set"));

結果

[CCC, AAA, BBB]

複数の要素を一度に追加可能

redisTemplate.delete("my_set");
ops.add("my_set", "AAA", "BBB", "CCC");
System.out.println(ops.members("my_set"));

結果

[CCC, AAA, BBB]

pop()
pop()でランダムに要素を削除して取得。

System.out.println(ops.pop("my_set"));
System.out.println(ops.members("my_set"));

結果

BBB
[CCC, AAA]

isMember()
isMember()で指定した要素が格納されているかどうかを返す。

redisTemplate.delete("my_set");
ops.add("my_set", "AAA", "BBB", "CCC");
Boolean existAAA = ops.isMember("my_set", "AAA");
System.out.println(existAAA);
Boolean existDDD = ops.isMember("my_set", "DDD");
System.out.println(existDDD);

結果

true
false

remove()
remove()で指定した要素を削除.

redisTemplate.delete("my_set");
ops.add("my_set", "AAA", "BBB", "CCC");
System.out.println(ops.members("my_set"));
ops.remove("my_set", "BBB");
System.out.println(ops.members("my_set"));

結果

[CCC, AAA, BBB]
[CCC, AAA]
HashOperations

RedisのHash型はHashOperationsを使用します。

HashOperations<String, Object, Object> ops = redisTemplate.opsForHash();

put(), get()
put()で指定したフィールドに値を設定。
get()で指定したフィールドの値を取得。

ops.put("my_hash", "aaa", "111");
ops.put("my_hash", "bbb", "222");
ops.put("my_hash", "ccc", "333");
System.out.println(ops.get("my_hash", "aaa"));

結果

111

putAll()
putAll()で指定したフィールドと値をまとめて設定。

redisTemplate.delete("my_hash");
Map<String, String> map = new HashMap<>();
map.put("aaa", "111");
map.put("bbb", "222");
map.put("ccc", "333");
ops.putAll("my_hash", map);
ops.entries("my_hash").forEach((key, value) -> System.out.println(key + ":" + value));

結果

bbb:222
aaa:111
ccc:333

keys(), values(), multiGet(), entries()
keys()ですべてのキーを取得。
values()ですべての値を取得
multiGet()でキーのコレクションを指定すると、それらのキーに対する値を取得
entries()で指定したキーのすべてのフィールドと値を取得。

System.out.println(ops.get("my_hash", "bbb"));
System.out.println(ops.keys("my_hash"));
System.out.println(ops.values("my_hash"));
ops.multiGet("my_hash", ops.keys("my_hash")).forEach(System.out::println);
ops.entries("my_hash").forEach((key, value) -> System.out.println(key + ":" + value));

結果

# get
222
# keys
[aaa, ccc, bbb]
# values
[111, 333, 222]
# multiGet
111
333
222
# entries
aaa:111
bbb:222
ccc:333

delete()
delete()で指定したフィールドを削除。

ops.entries("my_hash").forEach((key, value) -> System.out.println(key + ":" + value));
ops.delete("my_hash", "ccc");
ops.entries("my_hash").forEach((key, value) -> System.out.println(key + ":" + value));

結果

bbb:222
aaa:111
ccc:333

bbb:222
aaa:111

increment()
increment()で第2引数で指定したフィールドの値を、第3指定指定した数だけインクリメント。

System.out.println(ops.get("my_hash", "aaa"));
ops.increment("my_hash", "aaa", 1);
ops.increment("my_hash", "aaa", 1);
System.out.println(ops.get("my_hash", "aaa"));
ops.increment("my_hash", "aaa", 10);
System.out.println(ops.get("my_hash", "aaa"));

結果

111
113
123
ZSetOperations

RedisのZSet型はZSetOperationsを使用します。

ZSetOperations<String, String> ops = redisTemplate.opsForZSet();

add()
add()でzsetに第3引数で指定したスコアで、第2引数のメンバを登録。

ops.add("my_zset", "member1", 111);
ops.add("my_zset", "member2", 222);
ops.add("my_zset", "member3", 333);
ops.rangeWithScores("my_zset", 0, -1)
        .forEach(t -> System.out.println(t.getValue() + " : " + t.getScore()));

結果

member1 : 111.0
member2 : 222.0
member3 : 333.0

TypedTupleのSetを渡すことでまとめて登録も可能

redisTemplate.delete("my_zset");
Set<TypedTuple<String>> set = new HashSet<>();
set.add(new DefaultTypedTuple<>("member1", 111D));
set.add(new DefaultTypedTuple<>("member2", 222D));
set.add(new DefaultTypedTuple<>("member3", 333D));
ops.add("my_zset", set);
ops.rangeWithScores("my_zset", 0, -1)
        .forEach(t -> System.out.println(t.getValue() + " : " + t.getScore()));

結果

member1 : 111.0
member2 : 222.0
member3 : 333.0

range(), rangeWithScores()
range()で指定した範囲のメンバを返す。
第2引数は開始するインデックス、第3引数は終了するインデックス。
rangeWithScores()でスコアも同時に返す。

System.out.println(ops.range("my_zset", 0, -1));
ops.rangeWithScores("my_zset", 0, -1)
        .forEach(t -> System.out.println(t.getValue() + " : " + t.getScore()));

結果

[member1, member2, member3]
member1 : 111.0
member2 : 222.0
member3 : 333.0

rangeByScore(), rangeByScoreWithScores()
rangeByScore()で第2引数と第3引数の間のスコアを持つ要素を返す。
rangeByScoreWithScores()でスコアも同時に返す。

System.out.println(ops.rangeByScore("my_zset", 0, 150));
System.out.println(ops.rangeByScore("my_zset", 100, 300));
ops.rangeByScoreWithScores("my_zset", 0, 150)
        .forEach(t -> System.out.println(t.getValue() + " : " + t.getScore()));
ops.rangeByScoreWithScores("my_zset", 100, 230)
        .forEach(t -> System.out.println(t.getValue() + " : " + t.getScore()));

結果

[member1]

[member1, member2]

member1 : 111.0

member1 : 111.0
member2 : 222.0

redis-cliだと、第2引数と第3引数には無限大(+inf/-inf)を指定することが可能だが、
redisTemplateではDouble.MIN_VALUE/MAX_VALUEを使用する

System.out.println(ops.rangeByScore("my_zset", Double.MIN_VALUE, 200));
System.out.println(ops.rangeByScore("my_zset", 200, Double.MAX_VALUE));

結果

[member1]
[member2, member3]

remove()
remove()で指定されたメンバを削除する。

System.out.println(ops.range("my_zset", 0, -1));
ops.remove("my_zset", "member1");
System.out.println(ops.range("my_zset", 0, -1));

結果

[member1, member2, member3]
[member2, member3]

removeRangeByScore()
removeRangeByScore()で第2引数と第3引数の間のスコアを持つ要素を削除する。

redisTemplate.delete("my_zset");
ops.add("my_zset", set);
ops.rangeWithScores("my_zset", 0, -1)
        .forEach(t -> System.out.println(t.getValue() + " : " + t.getScore()));
ops.removeRangeByScore("my_zset", 200, 400);
ops.rangeWithScores("my_zset", 0, -1)
        .forEach(t -> System.out.println(t.getValue() + " : " + t.getScore()));

結果

member1 : 111.0
member2 : 222.0
member3 : 333.0

member1 : 111.0

Redis Client

Redis Clientとしてjedis以外にもlettuceもサポートしています。

dependency

クライアントとしてlettuceも試すため、依存関係にlettuceを追加しました。

pom.xml

<dependency>
    <groupId>biz.paluch.redis</groupId>
    <artifactId>lettuce</artifactId>
    <version>4.4.0.Final</version>
</dependency>
Configuration

AutoConfigurationを見ると、defaultではjedisがRedisConnectionFactoryとしてBean定義されてるようです。
lettuceの依存関係があるとlettuceもRedisConnectionFactoryとしてBean定義されるようです。

JedisConnectionFactoryとLettuceConncectionFactoryをBean定義して、
それぞれでStringRedisTemplateをBean定義して試してみます。

SpringDataRedisClientConfig.java

@Configuration
public class SpringDataRedisClientConfig {
    @Bean
    public StringRedisTemplate jedisRedisTemplate(JedisConnectionFactory jedisConnectionFactory) {
        StringRedisTemplate redisTemplate = new StringRedisTemplate();
        redisTemplate.setConnectionFactory(jedisConnectionFactory);
        return redisTemplate;
    }

    @Bean
    public StringRedisTemplate lettuceRedisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
        StringRedisTemplate redisTemplate = new StringRedisTemplate();
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        return redisTemplate;
    }

    @Bean
    public JedisConnectionFactory jedisConnectionFactory() {
        return new JedisConnectionFactory();
    }

    @Bean
    public LettuceConnectionFactory lettuceConnectionFactory() {
        return new LettuceConnectionFactory();
    }
}
Jedis

@QualifierでjedisRedisTemplateを指定して使ってみます。

    @Autowired
    @Qualifier("jedisRedisTemplate")
    private StringRedisTemplate jedisRedisTemplate;

    @Test
    public void jedisTest() {
        System.out.println(jedisRedisTemplate.getConnectionFactory());

        jedisRedisTemplate.delete("jedis");
        jedisRedisTemplate.opsForValue().set("jedis", "jedisTemplate");
        String got = jedisRedisTemplate.opsForValue().get("jedis");
        assertThat(got).isEqualTo("jedisTemplate");
    }

結果

org.springframework.data.redis.connection.jedis.JedisConnectionFactory@bf1ec20

JedisConnectionFactoryが使われているのが分かります。

Lettuce

@QualifierでlettuceRedisTemplateを指定して使ってみます。

    @Autowired
    @Qualifier("lettuceRedisTemplate")
    private StringRedisTemplate lettuceRedisTemplate;

    @Test
    public void lettuceTest() {
        System.out.println(lettuceRedisTemplate.getConnectionFactory());

        lettuceRedisTemplate.delete("lettuce");
        lettuceRedisTemplate.opsForValue().set("lettuce", "lettuceTemplate");
        String got = lettuceRedisTemplate.opsForValue().get("lettuce");
        assertThat(got).isEqualTo("lettuceTemplate");
    }

結果

org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory@636e8cc

LettuceConnectionFactoryが使われているのが分かります。

リアライザ

Redisへデータを格納する際のシリアライザも指定出来ます。

Configuration

リアライザはKeyとValueでそれぞれ指定できます。
指定する場合は
RedisTemplate.setKeySerializer()
RedisTemplate.setValueSerializer()
でそれぞれ指定します。

valueのシリアライザを、
・JdkSerializationRedisSerializer(デフォルト)
・StringRedisSerializer
・Jackson2JsonRedisSerializer
でそれぞれ指定して試してみます。
(keyのシリアライザはすべてStringRedisSerializer)

Jackson2JsonRedisSerializerではシリアライズするUserクラスを指定しています。

SpringDateRedisSerializeConfig.java

@Configuration
public class SpringDateRedisSerializeConfig {

    // JdkSerializationRedisSerializer(default)
    @Bean
    public RedisTemplate<String, User> redisTemplateDefault(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, User> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }

    // StringRedisSerializer
    @Bean
    public RedisTemplate<String, String> redisTemplateStringSerialize(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        return redisTemplate;
    }

    // Jackson2JsonRedisSerializer
    @Bean
    public RedisTemplate<String, User> redisTemplateJacksonSerialize(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, User> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(User.class));
        return redisTemplate;
    }

    @Bean
    public JedisConnectionFactory jedisConnectionFactory() {
        return new JedisConnectionFactory();
    }
}

シリアライズ対象として下記のUserクラスを定義します。
UserクラスをvalueにしてRedisに格納してみます。
(Serializableを実装している必要あり)

User.java

@Data
@AllArgsConstructor
public static class User implements Serializable {
    private String name;
    private int age;
}
JdkSerializationRedisSerializer(default)

何も指定しないとkeyもvalueもデフォルトはJdkSerializationRedisSerializerになります。
keyもJdkSerializationRedisSerializerにするとredisから操作しにくくなるので
keyはStringRedisSerializerにしておいたほうがよさそうです。

@Autowired
@Qualifier("redisTemplateDefault")
private RedisTemplate<String, User> redisTemplateDefault;

@Test
public void defaultSerialize() {
    System.out.println(redisTemplateDefault.getConnectionFactory());
    System.out.println(redisTemplateDefault.getKeySerializer().getClass().getName());
    System.out.println(redisTemplateDefault.getValueSerializer().getClass().getName());

    User alice = new User("alice", 20);
    redisTemplateDefault.delete("alice");
    redisTemplateDefault.opsForValue().set("alice", alice);

    User deserializedAlice = redisTemplateDefault.opsForValue().get("alice");

    assertThat(deserializedAlice).isEqualTo(alice);
}

結果

org.springframework.data.redis.connection.jedis.JedisConnectionFactory@65a15628
org.springframework.data.redis.serializer.StringRedisSerializer
org.springframework.data.redis.serializer.JdkSerializationRedisSerializer

redis-cliでどのように格納されているか確認

> get "alice"
"\xac\xed\x00\x05sr\x00Icom.example.redis.data.spring.serialize.SpringDateRedisSerializeTest$User\x85\xc8\x91_\xeb}\x85J\x02\x00\x02I\x00\x03ageL\x00\x04namet\x00\x12Ljava/lang/String;xp\x00\x00\x00\x14t\x00\x05alice"
StringRedisSerializer

StringRedisSerializerを指定するとtoStringしたような形でシリアライズされます。

@Autowired
@Qualifier("redisTemplateStringSerialize")
private RedisTemplate<String, String> redisTemplateStringSerialize;

@Test
public void stringSerialize() {
    System.out.println(redisTemplateStringSerialize.getConnectionFactory());
    System.out.println(redisTemplateStringSerialize.getKeySerializer().getClass().getName());
    System.out.println(redisTemplateStringSerialize.getValueSerializer().getClass().getName());

    User bob = new User("bob", 33);
    redisTemplateStringSerialize.delete("bob");
    redisTemplateStringSerialize.opsForValue().set("bob", bob.toString());

    String deserializedBob = redisTemplateStringSerialize.opsForValue().get("bob");

    assertThat(deserializedBob).isEqualTo(bob.toString());
}

結果

org.springframework.data.redis.connection.jedis.JedisConnectionFactory@65a15628
org.springframework.data.redis.serializer.StringRedisSerializer
org.springframework.data.redis.serializer.StringRedisSerializer

redis-cliでどのように格納されているか確認

> get "bob"
"SpringDateRedisSerializeTest.User(name=bob, age=33)"
Jackson2JsonRedisSerializer

Jackson2JsonRedisSerializerを指定すると、JacksonでJSON形式にシリアライズされます。

@Autowired
@Qualifier("redisTemplateJacksonSerialize")
private RedisTemplate<String, User> redisTemplateJacksonSerialize;

@Test
public void jacksonSerialize() {
    System.out.println(redisTemplateJacksonSerialize.getConnectionFactory());
    System.out.println(redisTemplateJacksonSerialize.getKeySerializer().getClass().getName());
    System.out.println(redisTemplateJacksonSerialize.getValueSerializer().getClass().getName());

    User cindy = new User("cindy", 44);
    redisTemplateJacksonSerialize.delete("cindy");
    redisTemplateJacksonSerialize.opsForValue().set("cindy", cindy);

    User deserializedCindy = redisTemplateJacksonSerialize.opsForValue().get("cindy");

    assertThat(deserializedCindy).isEqualTo(cindy);
}

結果

org.springframework.data.redis.connection.jedis.JedisConnectionFactory@65a15628
org.springframework.data.redis.serializer.StringRedisSerializer
org.springframework.data.redis.serializer.JdkSerializationRedisSerializer

redis-cliでどのように格納されているか確認

> get "cindy"
"{\"name\":\"cindy\",\"age\":44}"

Cache Abstraction(Spring Cache)

以前Cache Abstractionを試しましたが、
Spring Data RedisではCache Abstraction(Spring Cache)と連携出来ます。

dependency

依存関係にはspring-boot-starter-cacheを追加しました。

pom.xml

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

キャッシュを有効にするために@Configurationクラスに@EnableCachingを指定します。

デフォルトのRedisCacheManagerと、expireの設定をしたRedisCacheManagerをBean定義しました。
expireを設定するにはsetExpires()でキャッシュ名とexpire(秒)のMapを指定します。

リアライザにはJackson2JsonRedisSerializerを指定しました。

SpringDataRedisCacheConfig.java

@EnableCaching
@Configuration
@ComponentScan("com.example.redis.data.spring.cache")
public class SpringDataRedisCacheConfig {
    @Primary
    @Bean
    public RedisCacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) {
        return new RedisCacheManager(redisTemplate);
    }

    @Bean(name = "expireManager")
    public RedisCacheManager cacheManagerWithExpire(RedisTemplate<String, Object> redisTemplate) {
        RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
        // set expire
        Map<String, Long> expireMap = new HashMap<>();
        expireMap.put("expirePersonCache", 10L);
        cacheManager.setExpires(expireMap);
        return cacheManager;
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Person.class));
        return redisTemplate;
    }

    @Bean
    public JedisConnectionFactory jedisConnectionFactory() {
        return new JedisConnectionFactory();
    }
}
@Cacheable

キャッシュ対象のPersonクラスを作成。
(Serializableを実装する必要があります)
(all args constructorがないとjsonからデシリアライズ出来ないです)

@AllArgsConstructor
public static class Person implements Serializable {
    @Getter
    private String name;
    @Getter
    private int randomValue;

    private static Random random = new Random();

    Person(String name) {
        this.name = name;
        this.randomValue = random.nextInt();
    }
}

@Cacheableをメソッドに付与すると、結果をキャッシュします。
メソッドの引数がキャッシュのキーになります。
key属性を指定するとキャッシュのキーとなる引数を指定したり、文字列と連結することが出来ます。
cacheNames属性でキャッシュに名前を付けれます。

擬似的に遅くするためsleepを入れています。

PersonService.java

@Service
public class PersonService {

    @Cacheable(cacheNames = "personCache", key = "'Person:' + #name")
    public Person createPerson(String name) throws InterruptedException {
        Thread.sleep(3_000L);
        return new Person(name);
    }

         :
         :
}

確認

@Test
public void cacheTest() throws InterruptedException {
    // キャッシュなし
    long start = System.currentTimeMillis();
    Person andy = personService.createPerson("andy");
    long end = System.currentTimeMillis();
    System.out.println("no cache [" + (end - start) + "msec]");

    // キャッシュヒット
    long start2 = System.currentTimeMillis();
    Person cachedAndy = personService.createPerson("andy");
    long end2 = System.currentTimeMillis();
    System.out.println("cache hit [" + (end2 - start2) + "msec]");

    assertThat(andy.getName()).isEqualTo(cachedAndy.getName());
    assertThat(andy.getRandomValue()).isEqualTo(cachedAndy.getRandomValue());

2回めのアクセスは高速化されていることが分かります。

no cache [3168msec]
cache hit [34msec]

redis-cliでどのように格納されているか確認

> get "Person:andy"
"{\"name\":\"andy\",\"randomValue\":-1896696146}"
@Cacheable(expire)

expireを設定したcacheManagerを確認してみます。
cacheManager属性でexpireManagerを指定します。

PersonService.java

@Service
public class PersonService {

    @Cacheable(cacheManager = "expireManager", cacheNames = "expirePersonCache", key = "'ExpirePerson:' + #name")
    public Person createPersonWithExpire(String name) throws InterruptedException {
        Thread.sleep(3_000L);
        return new Person(name);
    }

         :
         :
}

確認

@Test
public void cacheWithExpireTest() throws InterruptedException {
    long start = System.currentTimeMillis();
    Person bobby = personService.createPersonWithExpire("bobby");
    long end = System.currentTimeMillis();
    System.out.println("no cache [" + (end - start) + "msec]");
}

redis-cliで確認してみます。
すぐにgetして確認したあと、数秒待って再度getして削除されていることを確認。

> get "ExpirePerson:bobby"
"{\"name\":\"bobby\",\"randomValue\":1551354967}"

#しばらくして再度get
> get "ExpirePerson:bobby"
(nil)
@CacheEvict

@CacheEvictでキャッシュの値を削除できます。
@CacheEvictを付与したメソッドで引数をキーとしてキャッシュを削除します。
戻り値はvoidを指定しています。戻り値があっても無視されます。

PersonService.java

@Service
public class PersonService {

    @CacheEvict(cacheNames = {"personCache", "expirePersonCache"})
    public void evict(String key) {
    }

         :
         :
}

確認。
キャッシュしてしばらくsleepしたあと、evictしてみます。

@Test
public void evictTest() throws InterruptedException {
    Person cindy = personService.createPerson("cindy");

    Thread.sleep(5_000L);

    personService.evict("Person:cindy");
}

redis-cliで確認してみます。
すぐにgetして確認したあと、数秒待って再度getして削除されていることを確認。

> get Person:cindy
"{\"name\":\"cindy\",\"randomValue\":1872046765}"

> get Person:cindy
(nil)

トランザクション

RedisTemplateではRedisのトランザクションをサポートしています。
また、@Transactionalにも対応しています。

Configuration

トランザクションサポートはデフォルトで無効になっているので、
setEnableTransactionSupport(true)で有効にします。

TransactionManagerのDataSourceの実装をどこから取得すればよいか分からなかったのでMockを使用。

SpringDataRedisTransactionConfig.java

@Configuration
public class SpringDataRedisTransactionConfig {
    @Bean
    public StringRedisTemplate redisTemplate() {
        StringRedisTemplate template = new StringRedisTemplate(redisConnectionFactory());
        template.setEnableTransactionSupport(true);
        return template;
    }

    @Bean
    public JedisConnectionFactory redisConnectionFactory() {
        return new JedisConnectionFactory();
    }

    @Bean
    public PlatformTransactionManager transactionManager() throws SQLException {
        return new DataSourceTransactionManager(dataSource());
    }

    @Bean
    public DataSource dataSource() throws SQLException {

        DataSource ds = Mockito.mock(DataSource.class);
        Mockito.when(ds.getConnection()).thenReturn(Mockito.mock(Connection.class));
        return ds;
    }
}
MULTI~EXEC

RedisTemplateのmulti()、exec()でトランザクションを試してみます。
トランザクションの間にsleepを入れて、exec()する前のデータがredis-cliで取得出来ない事を確認してみます。

@Test
public void multiExecTest() throws InterruptedException {
    // transaction start
    redisTemplate.multi();

    redisTemplate.opsForValue().set("my_key", "my_value");
    redisTemplate.opsForValue().increment("counter", 1L);
    redisTemplate.opsForValue().increment("counter", 1L);

    Thread.sleep(10_000L);

    redisTemplate.opsForValue().increment("counter", 1L);

    List<Object> results = redisTemplate.exec();
    // transaction end

    System.out.println(redisTemplate.opsForValue().get("my_key"));
    System.out.println(redisTemplate.opsForValue().get("counter"));

    System.out.println("-----------------");
    results.forEach(System.out::println);
    System.out.println("-----------------");
}

確認すると、exec()前にはデータが取得できず、exec()後にデータが取得出来ています。

# exec()前に取得
> get my_key
(nil)
> get counter
(nil)

# exec()後に再度取得
> get my_key
"my_value"
> get counter
"3"

結果。
exec()の戻り値でredisからの結果がシリアライズされて取得出来ます。

my_value
3
-----------------
1
2
3
-----------------
DISCARD

RedisTemplateのdiscard()でトランザクションキューのコマンドを削除できます。

@Test
public void discardTest() {
    // transaction start
    redisTemplate.multi();

    redisTemplate.opsForValue().set("my_key", "my_value");
    redisTemplate.opsForValue().increment("counter", 1L);
    redisTemplate.opsForValue().increment("counter", 1L);
    redisTemplate.opsForValue().increment("counter", 1L);

    redisTemplate.discard();
    // transaction end

    System.out.println(redisTemplate.opsForValue().get("my_key"));
    System.out.println(redisTemplate.opsForValue().get("counter"));
}

結果

null
null

redis-cliから確認してみると、トランザクションが実行されてないことが分かります。

> get "my_key"
(nil)
> get "counter"
(nil)
SessionCallback

RedisTemplateでは同一のコネクションでトランザクション内の操作が実行される保証はないので、
同じコネクションから操作したい場合はSessionCallbackを使用する必要があるようです。
SessionCallbackインターフェースでは、コネクション内で操作するコールバックを実装します。

@Test
public void useSessionCallbackTest() {
    List<Object> txResults = redisTemplate.execute(new SessionCallback<List<Object>>() {
        @Override
        @SuppressWarnings("unchecked")
        public <K, V> List<Object> execute(RedisOperations<K, V> operations) throws DataAccessException {
            operations.multi();

            operations.opsForValue().set((K)"my_key", (V)"my_value");
            operations.opsForValue().increment((K)"counter", 1L);
            operations.opsForValue().increment((K)"counter", 1L);

            try {
                Thread.sleep(10_000L);
            } catch (InterruptedException ignore) {
            }

            operations.opsForValue().increment((K)"counter", 1L);

            return operations.exec();
        }
    });
    System.out.println(redisTemplate.opsForValue().get("my_key"));
    System.out.println(redisTemplate.opsForValue().get("counter"));

    System.out.println("-----------------");
    txResults.forEach(System.out::println);
    System.out.println("-----------------");
}

確認すると、exec()前にはデータが取得できず、exec()後にデータが取得出来ています。

# exec()前に取得
> get my_key
(nil)
> get counter
(nil)

# exec()後に再度取得
> get my_key
"my_value"
> get counter
"3"

結果。
execute()の戻り値でredisからの結果がシリアライズされて取得出来ます。

my_value
3
-----------------
1
2
3
-----------------
@Transactional

@Transactionalもサポートされているので、宣言的トランザクションで管理出来ます。

@BeforeTransactionと@AfterTransactionでトランザクションの前後にcounterの値を出力。

@BeforeTransaction
public void before() {
    redisTemplate.delete("counter");
    System.out.println(redisTemplate.opsForValue().get("counter"));
}

@AfterTransaction
public void after() {
    System.out.println(redisTemplate.opsForValue().get("counter"));
}

下記のテストコードを記述。
@Transactionalを指定してます。
@Rollback(false)でテスト実行後にトランザクションロールバックしないようにします。
(または@Commit)

@Transactional
@Rollback(false)
@Test
public void transactionalTest() throws InterruptedException {
    redisTemplate.opsForValue().increment("counter", 1);
    redisTemplate.opsForValue().increment("counter", 1);

    Thread.sleep(10_000L);

    redisTemplate.opsForValue().increment("counter", 1);
}

結果。

null
2017-11-30 01:03:25.998  INFO 4232 --- [           main] o.s.t.c.transaction.TransactionContext   : Began transaction (1) for test context ………
2017-11-30 01:03:36.024  INFO 4232 --- [           main] o.s.t.c.transaction.TransactionContext   : Committed transaction for test context ………
3

redis-cliでsleep前後で値を取得してみると、トランザクション完了前は値を取得出来ていないことが分かります。

> get counter
(nil)

> get counter
"3"

同じテストを
@Rollback(true)でテスト実行後にトランザクションロールバックするようにしてみます。

@Transactional
@Rollback(true)
@Test
public void rollbackTest() throws InterruptedException {
    redisTemplate.opsForValue().increment("counter", 1);
    redisTemplate.opsForValue().increment("counter", 1);

    Thread.sleep(10_000L);

    redisTemplate.opsForValue().increment("counter", 1);
}

結果

null
2017-11-30 01:09:30.445  INFO 12952 --- [           main] o.s.t.c.transaction.TransactionContext   : Began transaction (1) for test context ………
2017-11-30 01:09:40.467  INFO 12952 --- [           main] o.s.t.c.transaction.TransactionContext   : Rolled back transaction for test context ………
null

redis-cliでsleep前後で値を取得してみると、ロールバックされて値が取得出来ないことが分かります。

> get counter
(nil)

> get counter
(nil)


終わり。

テストコードは下記にあげました。
github.com

【参考】
SpringのCache AbstractionでRedisを使ってみる - CLOVER
Spring Data Redis におけるデフォルト設定の注意点 - なんとなくな Developer のメモ