Help us understand the problem. What is going on with this article?

Spring BootでSpring Data Redisを利用する

More than 3 years have passed since last update.

まずは動かす

依存関係の追加

  • 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

①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);
    }
  };
}
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした