Spring Data Redis におけるデフォルト設定の注意点
Spring Data Redis のデフォルト設定に関して、個人的に気になった点を挙げておきます。
- (1) キーと値に JdkSerializationRedisSerializer を適用
- (2) トランザクションサポートが無効化 (
enableTransactionSupport
= false)
今回使用したモジュールは以下。
- Spring Boot Starter Redis 1.2.5 ( Spring Data Redis 1.4.3 )
サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20150827/
はじめに
今回作成したサンプルの Gradle 用ビルド定義です。
spring-boot-gradle-plugin
を使わず、gradle run
で実行するようにしました。
build.gradle
apply plugin: 'application' def enc = 'UTF-8' tasks.withType(AbstractCompile)*.options*.encoding = enc mainClassName = 'sample.App' repositories { jcenter() } dependencies { compile 'org.springframework.boot:spring-boot-starter-redis:1.2.5.RELEASE' }
クラス構成は以下の通りです。
実行クラス src/main/java/sample/App.java
package sample; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.ComponentScan; import sample.repository.SampleRepository; @ComponentScan @EnableAutoConfiguration public class App implements CommandLineRunner { @Autowired private SampleRepository sampleRepository; @Override public void run(String... args) { ・・・ } public static void main(String... args) { SpringApplication.run(App.class, args); } }
設定クラス src/main/java/sample/config/AppConfig.java
package sample.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericToStringSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration public class AppConfig { @Bean public JedisConnectionFactory jedisConnectionFactory() { return new JedisConnectionFactory(); } @Bean public RedisTemplate<String, Integer> redisTemplate() { RedisTemplate<String, Integer> template = new RedisTemplate<>(); template.setConnectionFactory(jedisConnectionFactory()); ・・・ return template; } }
Redis 用リポジトリクラス src/main/java/sample/repository/SampleRepository.java
package sample.repository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.core.*; import org.springframework.stereotype.Repository; import java.util.function.IntUnaryOperator; @Repository public class SampleRepository { @Autowired private RedisTemplate<String, Integer> redisTemplate; public Integer load(String key) { return redisTemplate.opsForValue().get(key); } ・・・ }
(1) キーと値に JdkSerializationRedisSerializer を適用
デフォルト設定では、キーにも値にも JdkSerializationRedisSerializer
を適用するため、Java でシリアライズしたキーと値を Redis へ保存します。
例えば、redisTemplate.opsForValue().set("a1", 10)
(redisTemplate.boundValueOps("a1").set(10)
でも可) のように "a1" というキーに 10 という値をセットした後、Redis で実際の値を確認すると以下のようになりました。
redis-cli による確認結果1
127.0.0.1:6379> keys * 1) "\xac\xed\x00\x05t\x00\x02a1" 127.0.0.1:6379> get "\xac\xed\x00\x05t\x00\x02a1" "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x00\n"
"a1" が "\xac\xed\x00\x05t\x00\x02a1"
に、 10 が "\xac\xed\・・・valuexr\x00\x10java.lang.Number・・・"
になって保存されています。
文字列や数値をシリアライズせずに Redis へ保存したい場合は、RedisTemplate
の個々の serializer を変更します。
今回は以下のように設定しました。
型 | 適用する serializer | 設定先プロパティ |
---|---|---|
String | StringRedisSerializer | keySerializer |
Integer | GenericToStringSerializer | valueSerializer |
他にも、ハッシュを使う場合は hashKeySerializer や hashValueSerializer を変更し、同じ serializer を全てに適用する場合は defaultSerializer を変更します。
設定クラス src/main/java/sample/config/AppConfig.java
・・・ @Configuration public class AppConfig { ・・・ @Bean public RedisTemplate<String, Integer> redisTemplate() { RedisTemplate<String, Integer> template = new RedisTemplate<>(); template.setConnectionFactory(jedisConnectionFactory()); // キーの serializer を変更 template.setKeySerializer(new StringRedisSerializer()); // 値の serializer を変更 template.setValueSerializer(new GenericToStringSerializer<>(Integer.class)); return template; } }
serializer 変更後に redisTemplate.opsForValue().set("a1", 10)
で保存した内容は以下のようになりました。
redis-cli による確認結果2 (selializer 変更後)
127.0.0.1:6379> keys * 1) "a1" 127.0.0.1:6379> get a1 "10"
(2) トランザクションサポートが無効化
デフォルトで RedisTemplate
の enableTransactionSupport
が false
となっています。
この場合、以下のように RedisTemplate
の multi
と exec
メソッドを使用しても Redis のトランザクションは適用されず、exec
の実行時に JedisDataException: ERR EXEC without MULTI
エラーが発生します。
// 楽観ロックの適用 redisTemplate.watch(key); ・・・ // Redis トランザクションの開始 redisTemplate.multi(); ・・・ // Redis トランザクションの実施。enableTransactionSupport = false の場合はエラー redisTemplate.exec();
エラーが発生する原因は、multi
と exec
を異なる Redis 接続に対して実施するためです。 (つまり、セッションが異なっている)
もう少し詳しく説明すると、multi
や exec
等で個別に RedisConnectionFactory
から RedisConnection
を取得し処理を実施します。
Redis トランザクションを使用するには、次の 3通りが考えられます。
- (a) enableTransactionSupport を true へ変更 (トランザクションサポートを有効化)
- (b) SessionCallback を execute
- (c) 自前で bindConnection・unbindConnection を実施
(b) と (c) は enableTransactionSupport = false の設定でも適用できる方法です。 (enableTransactionSupport = true でも問題ありません)
(a) enableTransactionSupport を true へ変更
enableTransactionSupport
を true へ変更するのが最も簡単だと思います。
また、true へ変更すると @Transactional
アノテーションも使えるみたいです。
設定クラス src/main/java/sample/config/AppConfig.java
・・・ @Configuration public class AppConfig { ・・・ @Bean public RedisTemplate<String, Integer> redisTemplate() { RedisTemplate<String, Integer> template = new RedisTemplate<>(); template.setConnectionFactory(jedisConnectionFactory()); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericToStringSerializer<>(Integer.class)); // トランザクションサポートを有効化 template.setEnableTransactionSupport(true); return template; } }
Redis 用リポジトリクラス src/main/java/sample/repository/SampleRepository.java
・・・ @Repository public class SampleRepository { @Autowired private RedisTemplate<String, Integer> redisTemplate; ・・・ public Object updateWithCas1(String key, IntUnaryOperator func) { try { // 楽観ロック redisTemplate.watch(key); BoundValueOperations<String, Integer> valueOps = redisTemplate.boundValueOps(key); // 現在の値を取得 Integer value = valueOps.get(); redisTemplate.multi(); // 値の更新。enableTransactionSupport = true の場合はキューイングされる valueOps.set(func.applyAsInt(value)); // enableTransactionSupport = true の場合はキューイングした処理の実行、false の場合はエラー return redisTemplate.exec(); } catch (Exception e) { // InvalidDataAccessApiUsageException: ERR EXEC without MULTI System.out.println(e); } return null; } ・・・ }
実行クラス src/main/java/sample/App.java
・・・ @ComponentScan @EnableAutoConfiguration public class App implements CommandLineRunner { @Autowired private SampleRepository sampleRepository; @Override public void run(String... args) { String key = "a1"; sampleRepository.save(key, 10); Object res1 = sampleRepository.updateWithCas1(key, v -> v + 5); System.out.println("res1 = " + res1); System.out.println(sampleRepository.load(key)); ・・・ } ・・・ }
実行結果は以下の通りです。
実行結果1
> groovy run ・・・ res1 = [] 15 ・・・
enableTransactionSupport = false の場合の実行結果は以下の通りです。
トランザクションを使用せず set a1 15
を単独で実行しています。
実行結果2 (enableTransactionSupport = false の場合)
> groovy run ・・・ org.springframework.dao.InvalidDataAccessApiUsageException: ERR EXEC without MULTI; nested exception is redis.clients.jedis.exceptions.JedisDataException: ERR EXEC without MULTI res1 = null 15 ・・・
(b) SessionCallback を execute
enableTransactionSupport = false の場合でも SessionCallback
(RedisCallback
でも可) を execute
すればトランザクションを使用できます。
SessionCallback
インターフェースの execute
メソッド内へ実装した処理は同一セッション内で実行されます。
ただし、SessionCallback
はメソッドへ仮型引数 K・V が付いており、API 的に微妙な気がします。 (SessionCallback<T, K, V>
の方がよかったのでは)
org.springframework.data.redis.core.SessionCallback
・・・ public interface SessionCallback<T> { <K, V> T execute(RedisOperations<K, V> operations) throws DataAccessException; }
SessionCallback
をラムダで代用したいところですが、仮型引数 K・V のせいで断念しました。
また、operations
(RedisOperations<K, V>
型) を強引にキャストしていますが、実体は redisTemplate
なので一応は問題無いはずです。
Redis 用リポジトリクラス src/main/java/sample/repository/SampleRepository.java
・・・ @Repository public class SampleRepository { @Autowired private RedisTemplate<String, Integer> redisTemplate; ・・・ public Object updateWithCas2(String key, IntUnaryOperator func) { // SessionCallback の実行 return redisTemplate.execute(new SessionCallback<Object>() { @Override public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException { // 扱い難いのでキャスト @SuppressWarnings("unchecked") RedisOperations<String, Integer> ops = (RedisOperations<String, Integer>)operations; ops.watch(key); BoundValueOperations<String, Integer> valueOps = ops.boundValueOps(key); // 現在の値を取得。 // multi 実行前に実施する必要あり。multi 後に実行するとキューイングされて戻り値が null になる Integer value = valueOps.get(); ops.multi(); valueOps.set(func.applyAsInt(value)); return ops.exec(); } }); } ・・・ }
実行クラス src/main/java/sample/App.java
・・・ @ComponentScan @EnableAutoConfiguration public class App implements CommandLineRunner { @Autowired private SampleRepository sampleRepository; @Override public void run(String... args) { String key = "a1"; ・・・ sampleRepository.save(key, 10); Object res2 = sampleRepository.updateWithCas2(key, v -> v + 10); System.out.println("res2 = " + res2); System.out.println(sampleRepository.load(key)); ・・・ } ・・・ }
設定クラス src/main/java/sample/config/AppConfig.java
・・・ @Configuration public class AppConfig { ・・・ @Bean public RedisTemplate<String, Integer> redisTemplate() { RedisTemplate<String, Integer> template = new RedisTemplate<>(); template.setConnectionFactory(jedisConnectionFactory()); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericToStringSerializer<>(Integer.class)); //template.setEnableTransactionSupport(true); return template; } }
実行結果は以下の通りです。
実行結果
> groovy run ・・・ res2 = [] 20 ・・・
(c) 自前で bindConnection・unbindConnection を実施
RedisConnectionUtils
の bindConnection
と unbindConnection
メソッドを使って、自前でセッションを制御する方法です。
Redis 用リポジトリクラス src/main/java/sample/repository/SampleRepository.java
・・・ @Repository public class SampleRepository { @Autowired private RedisTemplate<String, Integer> redisTemplate; ・・・ public Object updateWithCas3(String key, IntUnaryOperator func) { // Redis 接続を bind RedisConnectionUtils.bindConnection(redisTemplate.getConnectionFactory()); try { redisTemplate.watch(key); BoundValueOperations<String, Integer> valueOps = redisTemplate.boundValueOps(key); Integer value = valueOps.get(); redisTemplate.multi(); valueOps.set(func.applyAsInt(value)); return redisTemplate.exec(); } finally { // Redis 接続を unbind RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory()); } } }
実行クラス src/main/java/sample/App.java
・・・ @ComponentScan @EnableAutoConfiguration public class App implements CommandLineRunner { @Autowired private SampleRepository sampleRepository; @Override public void run(String... args) { String key = "a1"; ・・・ sampleRepository.save(key, 10); Object res3 = sampleRepository.updateWithCas3(key, v -> v + 15); System.out.println("res3 = " + res3); System.out.println(sampleRepository.load(key)); } }
実行結果は以下の通りです。
実行結果
> groovy run ・・・ res3 = [] 25 ・・・
備考 (Redis のトランザクション)
Redis のトランザクションは、MULTI ~ EXEC 間の処理(コマンド)をキューイングし、EXEC 時に直列化して処理します。(他の処理が途中で入り込む事はない)
ロールバックはできず、途中でコマンドが失敗(文法エラーなど)しても次のコマンドを引き続き実行する点に注意が必要です。
なお、EXEC の代わりに DISCARD を実行するとキューが破棄されトランザクションはキャンセルされます。
WATCH は check-and-set (CAS) による楽観ロックをトランザクションへ適用します。
WATCH したキーが、WATCH 後に更新されていれば MULTI ~ EXEC によるトランザクションを EXEC 時に中止します。
また、EXEC すると WATCH が解除されて UNWATCH になります。
WATCH の動作検証
watch の後に sleep 処理を差し込んで、楽観ロックとトランザクションの動作を確認してみます。
Redis 用リポジトリクラス src/main/java/sample/repository/SampleRepository.java
・・・ @Repository public class SampleRepository { @Autowired private RedisTemplate<String, Integer> redisTemplate; ・・・ public Object updateWithCas2(String key, IntUnaryOperator func) { return redisTemplate.execute(new SessionCallback<Object>() { @Override public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException { @SuppressWarnings("unchecked") RedisOperations<String, Integer> ops = (RedisOperations<String, Integer>)operations; ops.watch(key); // sleep try { Thread.sleep(10000); } catch(InterruptedException e) {} ・・・ ops.multi(); valueOps.set(func.applyAsInt(value)); return ops.exec(); } }); } public Object updateWithCas3(String key, IntUnaryOperator func) { RedisConnectionUtils.bindConnection(redisTemplate.getConnectionFactory()); try { redisTemplate.watch(key); ・・・ redisTemplate.multi(); // sleep try { Thread.sleep(10000); } catch(InterruptedException e) {} valueOps.set(func.applyAsInt(value)); return redisTemplate.exec(); } finally { RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory()); } } }
sleep している間に redis-cli で set a1 1
(キー "a1" へ 1 という値をセット) を実行してみたところ、以下のような結果となりました。
実行結果 (sleep 中に a1 の値を更新)
> gradle run ・・・ res2 = null 1 res3 = null 1 ・・・
redisTemplate.exec()
の戻り値が null
となり、トランザクションの中止を確認できました。 (値も更新されていません)