spring bootでredisにキャッシュする際のcache abstractionについて書くページ。
cache abstractionはキャッシュの抽象化が直訳だが、日本語では透過的キャッシュという言葉も使うらしい。Spirng4.1からJSR-107のキャッシュ用のアノテーションがフルサポートされている。

はじめに

Spring bootでSpring SessionとSpring Redisを使えば簡単にSessionをRedisに保存できるが、Session情報以外にもアプリケーションから高頻度で参照されるデータなどはRedis(メモリ)に置きたくなる。

RDBもキャッシュの仕組みがあるが、メモリにすべてのデータが乗っているわけではないのでメモリに無いデータはディスクから読むこととなる。対して、Redisの場合はすべてのデータがメモリにあるのでRedisのほうが高速にデータを取り出すことができる。

Spring boot 1.4.Xを使えばDBに保存されているデータをRedisにキャッシュする処理が記述できる。意外と簡単にできたのでこのページではその方法を記述する。

書くこと

DBに保存されているデータが初めてServiceなどから参照された時にそのデータをRedisにキャッシュして次回からはキャッシュされたRedis上のデータを参照する様に設定する。
要はデータがDBにあるのかキャッシュにあるかを意識せずにプログラミングすることができるようになります。

設定

ソースはこちら

dependency

  • Spring Boot 1.4.0

pom.xml

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

application.yml

MySQLとRedisは既にインストールしてあるものとする。

spring:
redis:
host: localhost
port: 6379
datasource:
url: jdbc:mysql://localhost:3306/sample_db
username: root
password: password
driver-class-name: com.mysql.jdbc.Driver

tableとEntitiy

Cacheするデータが入ったテーブルは以下。すご~く適当に作成。

mysql> select * from users;
+----+-------+------------------+----------+-----------+---------+----------+
| id | name | email | username | authority | enabled | password |
+----+-------+------------------+----------+-----------+---------+----------+
| 1 | ishii | ishii@ishii.tech | | NULL | NULL | NULL |
+----+-------+------------------+----------+-----------+---------+----------+

上記のテーブルに対応するEntityクラス。

@Data
@Entity
@Table(name = "users")
public class User implements Serializable {
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
private long id;
private String name;
private String email;
private String username;
private Boolean enabled;
private String authority;
private String password;
}

repository

Userを取り出すRepositoryは以下。適当なメソッドを書く。

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
public User findByName(String name);
public User findByEmail(String email);
}

Redis設定とService

ここからRedisへキャッシュするための設定を行う。

AutoConfigで設定されるデフォルトのSerializerはKeyに対してもJdkSerializationRedisSerializerが適用されてすごくわかりづらい。Java以外からも参照したい場合はStringRedisSerializerなどを適用して文字列としてRedisに保存するようにしておいたほうがいい。ここではKeyは文字列を適用している。

JedisConnectionFactoryでコネクションを生成しているが、RedisConnectionFactoryも使える。特に困ったことがないので違いを調べていないがここではJedisConnectionFactoryを使っている。

Json形式も扱えるがJsonの型がEntity毎にhogehogeJsonRedisTemplateみたいなものを作成する必要があるっぽい。とりあえずここでは文字列で保存するのでJsonの話はしない。

@Configuration
@EnableCaching
public class RedisConfig {
@Autowired
private JedisConnectionFactory jedisConnectionFactory;
@Bean
public RedisTemplate redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(jedisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public RedisCacheManager redisCacheManager() {
return new RedisCacheManager(redisTemplate());
}
}

戻り値をキャッシュしたいメソッドに@CacheableをつけることでRedisにキャッシュがなければDBからデータを取得してRedisにキャッシュすることができる。

@Service
@Transactional
public class UserService {
@Autowired
UserRepository userRepository;

@Cacheable(cacheNames = "User", key = "'User:' + #name")
public User findByName(String name){
return userRepository.findByName(name);
}
public User findByEmail(String email){
return userRepository.findByEmail(email);
}
}

@Cacheableはいくつかパラメータが設定できる。パラメータの詳細はこちら。

ここではキャッシュの名前としてUser、キャッシュのKeyを引数のnameとして設定している。Keyの設定にはSpELが使用できる。

Controller

上で作ったServiceを実行するために適当なControllerを作る。

@Controller
public class UserController {
@Autowired
UserService userService;

@RequestMapping(value = "/")
@ResponseBody
public String list() {
User user = userService.findByName("ishii");
return user.getName();
}
}

確認

Controllerに接続してRedisとDBに対するアクセスを確認してみる。

MySQLのクエリログとRedisのmonitorを表示しながら確認を行った。
書いてから思ったのですが、タイムスタンプの比較がわかりにくい。

$ tail -f /var/log/query.log
160902 1:02:59 2 Query SET autocommit=0
2 Query select user0_.id as id1_0_, user0_.authority as authorit2_0_, user0_.email as email3_0_, user0_.enabled as enabled4_0_, user0_.name as name5_0_, user0_.password as password6_0_, user0_.username as username7_0_ from users user0_ where user0_.name='ishii'
2 Query commit
2 Query SET autocommit=1
$ redis-cli monitor
1472745767.071485 [0 127.0.0.1:39530] "flushall"
1472745779.411708 [0 127.0.0.1:40090] "PING"
1472745779.540049 [0 127.0.0.1:40090] "EXISTS" "User~lock"
1472745779.541225 [0 127.0.0.1:40090] "GET" "User:ishii"
1472745779.548405 [0 127.0.0.1:40090] "EXISTS" "User~lock"
1472745779.549486 [0 127.0.0.1:40090] "MULTI"
1472745779.549496 [0 127.0.0.1:40090] "SET" "User:ishii" "\xac\xed\x00\x05sr\x00\x17com.example.entity.User\r\xd5D]w\xbbXX\x02\x00\aJ\x00\x02idL\x00\tauthorityt\x00\x12Ljava/lang/String;L\x00\x05emailq\x00~\x00\x01L\x00\aenabledt\x00\x13Ljava/lang/Boolean;L\x00\x04nameq\x00~\x00\x01L\x00\bpasswordq\x00~\x00\x01L\x00\busernameq\x00~\x00\x01xp\x00\x00\x00\x00\x00\x00\x00\x01pt\x00\x10ishii@ishii.techpt\x00\x05ishiipt\x00\x00"
1472745779.549526 [0 127.0.0.1:40090] "ZADD" "User~keys" "0.0" "User:ishii"
1472745779.549537 [0 127.0.0.1:40090] "EXEC"
1472745785.782579 [0 127.0.0.1:40090] "EXISTS" "User~lock"
1472745785.784189 [0 127.0.0.1:40090] "GET" "User:ishii"
1472745809.412183 [0 127.0.0.1:40090] "PING"

Controllerに初めてアクセスした時はキャッシュにデータが無いのでDBへの問い合わせが行われるが、2回目以降はRedisからデータが読み出され、DBへの問い合わせは行われなくなる。

まとめ

アノテーション1つで簡単にキャッシュの操作ができることが確認できた。読み出しのみ確認したが、削除と更新についてもアノテーションを付与するだけで簡単にキャッシュの操作ができるようになっている。

@CacheEvictが削除、@CachePutが更新で同じようにServiceの該当するメソッドに付与することでキャッシュの操作ができる。

おわり。

参考

  1. https://spring.io/guides/gs/caching/
  2. http://docs.spring.io/spring/docs/current/spring-framework-reference/html/cache.html
  3. http://blog.rakugakibox.net/entry/2015/07/27/spring-boot-with-redis
  4. http://qiita.com/yoshidan/items/f7c10a43d2a40c3ce8df