Kotlin + Spring BootでDBキャッシュをRedisでやってみる


はじめに

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を記述します。
ポートは全てデフォルトです。

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を作成します。

User.kt
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
UserRepository.kt
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とのやりとりを行うクラスを定義します。
同時にキャッシュを見て、ヒットすればキャッシュからデータを取得します。

UserService.kt
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の作成

UserController.kt
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クラスの作成

RedicConfig.kt
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の初期化

init.sql
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の内容をよく理解していないのでこれに関して何かご教授いただけますと幸いです。
ここまで読んでいただきましてありがとうございました!