로깅/Redis

Redis Transaction 처리 feat. Spring Boot + Kotlin

리프기어 2021. 1. 5. 21:07

 

Spring Boot + Kotlin 환경에서 Redis Transaction 처리에 대해 알아보고, 적용 과정에서 해결해야 했던 것들을 공유합니다.

1. 테스트 환경

  • Spring Boot Version : 2.3.1

2. Redis Transaction 처리

스프링 공식문서에는 Redis Transaction 처리 관련해서 2가지 방법을 소개합니다.

  1. @Transactional Support
  2. RedisTemplate의 multi, exec, discard 이용

* 스프링 공식문서

docs.spring.io/spring-data/data-redis/docs/2.3.4.RELEASE/reference/html/#tx

2.1. @Transactional Support

스프링 문서의 일부분입니다.

@Transactional을 사용하려면 3가지 설정을 해야 합니다.

  1. Redis configure에 @EnableTransactionsManagement 추가
  2. RedisTemplate에 transaction support 기능을 true로 변경 (기본값 : false)
  3. PlatformTransactionManager를 bean으로 등록

Redis의 PlatformTransactionManager는 redisson으로 사용 가능하다고 합니다.

dzone.com/articles/how-to-manage-transactions-in-redis-on-java

여기서는 다른 방법인 RedisTemplate의 multi, exec, discard 이용하여 transaction 처리를 해봤습니다.

2.2 RedisTemplate의 multi, exec, discard 이용

MULTI, EXEC, DISCARD, WATCH는 Redis transaction의 기본 요소들입니다.

  • MULTI : transaction의 시작입니다.
  • EXEC : transaction의 종료입니다. commit과 같다고 할 수 있습니다.
  • DISCARD : transaction을 중단합니다.
  • WATCH : Optimistic locking 기능을 제공합니다.

각 요소들의 자세한 사항은 redis 문서를 보시면 됩니다.

redis.io/topics/transactions

 

위 기능들을 사용하려면 redisTemplate의 execute 메서드를 이용하면 됩니다.

인터넷에 검색하면 대부분 JAVA로 된 예제 코드들입니다. 이 코드를 IntelliJ에서 Kotlin으로 변경하면 Generic type 때문에 오류가 발생합니다. 여기서는 아래와 같이 하여 에러를 해결했습니다.

redisTemplate.execute(object : SessionCallback<Any?> {
    @Throws(DataAccessException::class)
    override fun <K : Any?, V : Any?> execute(operations: RedisOperations<K, V>): Any? {
        try {
            operations.watch(key as K)  // WATCH: Optimistic Locking
            operations.multi()        // MULTI: 트랜잭션 경계 설정
            operations.opsForHash<K, V>().putAll(key as K, map as MutableMap<K, V>)
        } catch (e: Exception) {
            operations.discard()
            return null
        }
        return operations.exec()    // EXEC: COMMIT
    }
})

Redis에 HMSET으로 저장하려면 operations.opsForHash <K, V>(). putAll()을 이용해야 합니다.

이때, 다음과 같이 의도하지 않은 값들이 key와 value에 저장될 수 있습니다.

위와 같은 값이 나오면 redisTemplate에 serializer을 설정하여 해결할 수 있습니다.

val stringSerializer: RedisSerializer<String> = StringRedisSerializer()
redisTemplate.stringSerializer = stringSerializer
redisTemplate.keySerializer = stringSerializer
redisTemplate.hashKeySerializer = stringSerializer
redisTemplate.hashValueSerializer =stringSerializer

3. 그 외 실험

3.1 WATCH를 설정하고 안하고의 차이?

watch를 설정하고 안 하고 어떠한 차이가 날지 테스트해봤습니다.

테스트는 TTL update를 동시에 3번 시도하는 것으로 했습니다.

다음은 redis-cli에서 monitor를 캡처한 것입니다.

  • WATCH 미설정 시

PEXPIRE는 millisecond 만큼 TTL을 설정하겠다는 의미입니다. 여기서는 7,200,000 밀리초이니 2시간입니다.

watch를 설정하지 않으면 3번의 transaction이 시간차로 실행됩니다.

  • WATCH 설정시

watch를 설정하기 되면 TTL 설정은 한 번으로 끝나게 됩니다.