Spring BootでSpring Data Redisを利用する 


# まずは動かす

## 依存関係の追加

* mavenの場合pomにspring-boot-starterとspring-boot-starter-redisを追加する。
* Spring Data デフォルトではJedisを利用してRedisにアクセスする。

```
<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>1.3.0.M5</version>
</parent>
    
<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
  		<artifactId>spring-boot-starter</artifactId>
	</dependency>

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

## 設定

* 起動クラスに@SpringBootApplicationをつけるだけでspring-boot-autoconfigureが自動的にRedisTemplateを利用可能にしてくれる。
* デフォルトではlocalhost:6379に接続する。
* 設定を変更したければapplication.ymlに以下のように記載、コネクションプールの設定なども記載可能。

```
spring:
    data:
        redis:
            host: 192.168.33.11
            port: 6379   
```                        

## 利用方法

RedisTemplateを使ってRedisを操作する。

```
@Autowired
RedisTemplate redisTemplate;

public void write() {

	redisTemplate.opsForZSet().add(userId, itemId ,new Date().getTime()))

}

public List<History> read(long begin , long end) {
	return redisTemplate.opsForZSet().reverseRangeWithScores(null, begin , end))
        	.stream().map(e -> {
         	 History history = new History();
          	 history.setDate( new Date((long)(e.getScore())));
          	 history.setItemId( result.getValue());
          	 return history;
    }).collect(Collectors.toList());
}
```

* 上記はSortedSetを利用したサンプルです。SortedSetを利用することで以下のようなことが可能。
	- 商品IDの重複を許可しないため、同じ商品を何度見られても1件として扱われる
	- 2日前に商品Aを見た->1日前に商品Bを見た->今日商品Aをまた見た、の場合に商品Aを商品Bより優先する

# spring-contextのCacheと連携させる

今度はRedisをキャッシュとして使ってみる。

## 設定
* CachingConfigurerSupportクラスを継承してCacheManagerの設定を追加する。
* @EnableCachingアノテーションの付与も必要。

```
@Configuration
@EnableCaching
public class RedisConfiguration extends CachingConfigurerSupport {

	@Bean
	@Autowired
	public CacheManager cacheManager(RedisTemplate<Object,Object> redisTemplate>){

    	RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate)
		
		// キャッシュ有効期限の設定(秒)
		Map<String, Long> expires = new HashMap<String, Long>();
    	expires.put("cache.day", new Long(24 * 60 * 60));
    	expires.put("cache.short", new Long(3 * 60));
   	 	cacheManager.setExpires(expires);
   	 	return cacheManager;
	}
}  
```

## 利用方法

* Cacheableアノテーションを利用することでキャッシュに乗せることが可能。
* value属性には上のexpiresに設定した値を指定すると専用の有効期限が設定される。
* expiresに存在しない値にすると無期限（デフォルト）になる。
* key属性にはredisのキャッシュとして登録したいキーを記載する。EL式を記載することで引数の値を含んだキーとすることができる。

```
  @Cacheable(value = "cache.day", key = "'item/' #itemId")
  public Item find(String itemId) {
    // execute database sql
  }
```

* この例ではvalue属性にcache.dayを利用していため有効期限(Redisのttl)が24時間となる。
* 呼び出し元からfind("001")でコールしたときredisに登録されるkey値は「item/001」となる。
* item/001に対するvalueはsqlTemplate.find(itemId)の結果のオブジェクトであるItemをSerializeしたものになる。SerializerはRedisTemplateに指定することで変更可能。

## 内部動作

@Cacheableアノテーションをつけたメソッドに対してCacheInterceptorが働く。
下の図は上のメソッドを呼び出した時の流れ。

![RedisCache.001.png](https://qiita-image-store.s3.amazonaws.com/0/39230/5d1cc168-6f65-9c3d-10ae-c1f1806023bf.png "RedisCache.001.png")


```
①EXISTS cache.day~lock
```

これはロックを取るための仕組み。
cache.day~lockはRedisCache#clean処理が実行されている時に作成されるキーで、このキーが存在している間はRedisCache側でwaitする。
waitといってもこのキーが存在しなくなるまで300msec毎にEXIST投げるループしているだけ（spring data redis 1.6.0.RELEASEで確認)

```
②GET item/001
```
* キー「item/001」に該当するデータを取得する。
* 結果が存在しなければここで@Cacheableを付与したメソッドが呼び出される。空データでもキーが存在すればメソッドは呼び出されない。
* 結果が存在してれば呼び出し元にRedisから取得した値を返却して終了。

```
④EXISTS cache.day~lock
```

GET時と同じくSETのためにロックの有無を確認。

```
⑤MULTI〜⑨EXEC
```
* トランザクション開始します。ここから⑨EXECまでは同一トランザクションで処理される。
* ⑥で値をセット。
* ⑦ではkeyをcache.day~keysというSortedSetに登録。@Cacheableのvalue値毎にキーの一覧を管理する形になる。結果として有効期限毎にキーの一覧が管理されることになる。
* ⑧有効期限が設定されていればEXPIREコマンド発行。

## キャッシュの破棄

* @CacheEvictを付与したメソッドを呼ぶとDELする。
* 例えば以下のようにするとvalue属性を@Cacheableのvalueと同一にすることで対応するキャッシュを全て削除できる。

```
@CacheEvict(value = "cache.day", allEntries = true)
public void evict() {
	// delete DB data 
}
```

## Cacheエラー時の所作変更

CacheErrorHandlerをBeanに登録することでCacheに関するエラーが発生した時の振る舞いを定義することができる。

以下はCacheに関する全てのExceptionをログに出力して、呼び出し元にキャッシュによるエラーを意識させずに、必ず@Cacheable付与されたメソッドを呼び出すサンプル

```
@Bean
@Override
public CacheErrorHandler errorHandler() {
  return new CacheErrorHandler() {
    @Override
    public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
      LOGGER.error(String.format("%s:%s:%s", cache.getName(), key, exception.getMessage()),
            exception);
    }

    @Override
    public void handleCachePutError(RuntimeException exception, Cache cache, Object key,
          Object value) {
      LOGGER.error(String.format("%s:%s:%s", cache.getName(), key, exception.getMessage()),
            exception);
    }

    @Override
    public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
        LOGGER.error(String.format("%s:%s:%s", cache.getName(), key, exception.getMessage()),
            exception);
      LOGGER.error(exception.getMessage(), exception);
    }

    @Override
    public void handleCacheClearError(RuntimeException exception, Cache cache) {
      LOGGER.error(String.format("%s:%s", cache.getName(), exception.getMessage()), exception);
      LOGGER.error(exception.getMessage(), exception);
    }
  };
}
```