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