はじめに
Kotlin + Spring-BootのアプリケーションでRedisを使用してDB(MySQL)のデータをキャッシュしてみました。
環境
Spring Boot 2.2.6
Kotlin 1.3.71
gradle 6.3
プロジェクトの雛形作成
Spring Initializrにて雛形を作成します。
雛形の設定はこのような感じです。
https://start.spring.io/
#MySQLとRedisの設定
MySQLとRedisはインストール済であることを前提とします。
下記のようにapplication.propertiesを記述します。
ポートは全てデフォルトです。
#MySQL
spring.jpa.hibernate.ddl-auto=update
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/demo
spring.datasource.username=demouser
spring.datasource.password=password
#Redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=null
spring.redis.database=0
#EntityとRepositoryの作成
例としてこのようなEntityとRepositoryを作成します。
package com.example.demo.domain.entity
import java.io.Serializable
import java.util.*
import javax.persistence.*
@Entity
data class User (
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id : Int = 0,
var name : String,
var age : Int
) : Serializable
package com.example.demo.domain.repository
import com.example.demo.domain.entity.User
import org.springframework.data.jpa.repository.JpaRepository
interface UserRepository : JpaRepository<User, Int> {
}
#Serviceの作成
DBとのやりとりを行うクラスを定義します。
同時にキャッシュを見て、ヒットすればキャッシュからデータを取得します。
package com.example.demo.app.service
import com.example.demo.domain.repository.UserRepository
import com.example.demo.domain.entity.User
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.cache.annotation.Cacheable
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.util.*
@Service
@Transactional
class UserService{
@Autowired
lateinit var userRepository: UserRepository
fun existsById(id : Int) : Boolean {
return userRepository.existsById(id)
}
@Cacheable(cacheNames = ["User"], key = "'User:' + #id")//キャッシュの参照とミスヒット時の登録
fun findById(id : Int) : Optional<User> {
return userRepository.findById(id)
}
}
@Cacheable
アノテーションを付加した関数はreturnする前にキャッシュを参照します。指定されたキー(ex; User::User:1)のキャッシュが存在すればキャッシュから値を取得し、returnします。キャッシュが存在しない場合は、returnする値を指定されたキーにキャッシュします。
この例では記述していませんが@CachePut
アノテーションが付加された関数は、return時にreturnする値でキャッシュを更新します。例えばDBのカラムを編集した際に同時にキャッシュを更新したりする用途に使用します。
また@cacheEvict
はreturn時に指定したキーのキャッシュを削除します。DBのカラムを削除して同時にキャッシュも削除したい時などに使用します。
この例は、idを指定して、そのカラムを取得する際にキャッシュの参照と登録を行う関数を記述しています。
#Controllerの作成
package com.example.demo.app.controller
import com.example.demo.app.service.UserService
import com.example.demo.domain.entity.User
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.ResponseBody
import java.util.*
import kotlin.system.measureTimeMillis
@Controller
class UserController{
@Autowired
lateinit var userService : UserService
@GetMapping("/users/{id}")
@ResponseBody
fun getUser(@PathVariable id: Int) : String {
lateinit var user: Optional<User>
if (userService.existsById(id)) {
val time = measureTimeMillis {
user = userService.findById(id)
}
var name = user.get().name
var age = user.get().age
return "name=$name, age=$age, time=$time(ms)"
}
return "does not exist id=$id"
}
}
/users/{id}
にリクエストするとidが存在すればDBまたはキャッシュからのデータを取得します。その際にかかった時間(time)を計測しておきます。レスポンスとしてUserのname, ageと計測した時間timeを含む文字列を返します。idが存在しなければ存在しない旨を伝える文字列を返します。
#Redisのconfigクラスの作成
package com.example.demo.app.config
import org.springframework.cache.annotation.EnableCaching
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.cache.RedisCacheConfiguration
import org.springframework.data.redis.cache.RedisCacheManager
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer
import org.springframework.data.redis.serializer.StringRedisSerializer
@Configuration
@EnableCaching
class RedisConfig{
@Bean
fun redisConnectionFactory(): LettuceConnectionFactory {
return LettuceConnectionFactory()
}
@Bean
fun redisTemplateSet() : RedisTemplate<String, Object> {
var redisTemplate = RedisTemplate<String, Object>()
redisTemplate.setConnectionFactory(redisConnectionFactory())
redisTemplate.keySerializer = StringRedisSerializer()
redisTemplate.valueSerializer = JdkSerializationRedisSerializer()
redisTemplate.afterPropertiesSet()
return redisTemplate
}
@Bean
fun redisCacheManager(lettuceConnectionFactory : LettuceConnectionFactory) : RedisCacheManager {
var redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(lettuceConnectionFactory)
.cacheDefaults(redisCacheConfiguration).build();
}
}
今回の例では、key : "User::User:1"
に対して、valueはid:1のuserのデータクラス(id, name, age)をキャッシュします。つまりObjectを格納します。
#databaseの作成とtableの初期化
create database if not exists demo;
grant all on demo.* to 'demouser'@'%';
create table if not exists demo.user(
id INT(11) AUTO_INCREMENT not null primary key,
name varchar(30) not null,
age INT(3) not null
);
insert into demo.user(name, age)values('A-san', 20);
mysql -u root -p < init.sql
#実行してみる
アプリケーションを起動します。
./gradlew bootRun
別ターミナルでリクエストを送ってみます。
curl localhost:8080/users/1
#> name=A-san, age=20, time=268(ms)
1回目はキャッシュされておらずDBから直接データを取得するので268msと少し時間がかかっています。
そのままもう一度同じコマンドを入力します。
curl localhost:8080/users/1
#> name=A-san, age=20, time=9(ms)
2回目はキャッシュが効いているためかなり高速にデータが取得できています。
最後に
Kotlin + Spring Boot2でredisを使ってDBキャッシュしました。
この組み合わせでredisを使ってDBキャッシュをしようとして、特にRedisConfig.ktに記述した内容でハマってしまったので、参考になれば幸いです。私もまだRedisConfig.ktの内容をよく理解していないのでこれに関して何かご教授いただけますと幸いです。
ここまで読んでいただきましてありがとうございました!