[maxxlog] 스터디방 참여 동시성 제어 - Redis(Redisson)

maxxyoung·2024년 4월 9일
0

maxxlog

목록 보기
8/9

문제가 생길 수 있는 상황

스터디 방은 매달 1일에 다시 참여할 수 있다. 모든 스터디방의 참여 인원은 10명으로 제한한다.

  • 인기있는 스터디방 이라면 매달 1일, 다수의 사용자가 한 번에 가입을 시도할거라고 가정하고 있다.
  • 10명이 넘는 사용자가 동시에 가입을 시도한다면 10명이 넘는 인원이 스터디방에 가입할 수 있어 문제가 될 수 있다.

동시성 제어가 없는 상태로 멀티스레드를 만들어 테스트한 결과 원하는 결과보다 훨씬 더 많은 스터디방 참여자가 생겼다.

  • 테스트는 ExecutorServiceCountDownLatch를 사용했다.
@Test
    fun studyRoomJoinConcurrencyTest() {
        //given
        val threadCount = 50

        val category = categoryRepository.save(fixture<Category>())
        val studyRoom = studyRoomRepository.save(fixture<StudyRoom>() {
        property(StudyRoom::category) { category }
        property(StudyRoom::state) { StudyRoomState.ACTIVATED }
        })
        val studyRoomId = studyRoom.id
        val users = (1..threadCount).map { userRepository.save(fixture<User> { property(User::state) { UserState.ACTIVATED } }) }

        val executorService: ExecutorService = Executors.newFixedThreadPool(32)
        val latch = CountDownLatch(threadCount)

        //when
        for (user in users) {
            executorService.execute {
                val response = studyRoomService.joinStudyRoom(studyRoomId, user.id)
                latch.countDown()
            }
        }
        latch.await()

        //then
        val userCount = userStudyRoomRepository.countByStudyRoom(studyRoomId)
        Assertions.assertThat(userCount).isEqualTo(10)
    }

스터디방 참여 인원의 동시성 제어

해결방안으로 많은 방법이 있다. pessmistic lock, optimistic lock, named lock 등등이 있지만 나는 분산환경에서 빠른 성능을 기대해 볼 수 있는 redis를 활용해 동시성 제어를 해보겠다. 물론 최대 10명의 동시성을 처리하는데는 너무나도 오버스펙이지만 학습의 의의를 둬야한다.
레디스 클라이언트 라이브러리도 lettuce, redisson 두 개가 존재한다. 다음과 같은 이유로 redisson을 사용한다.

  • 락 획득 실패 시 재시도 해야한다.
  • 스핀 락 방식이 아닌 pub/sub 방식을 사용하여 레디스 서버의 부하를 줄일 수 있다.
  • 설정된 lease time이 지나면 락을 반납하여 데드락을 방지한다.

Redisson 의존성 추가

Redisson의 경우 따로 의존성 추가가 필요하다.

implementation("org.redisson:redisson-spring-boot-starter:3.21.1")

config파일을 작성한다.

@Configuration
class RedisRedissonConfig (
    @Value("\${spring.data.redis.host}")
    private val host: String,
    @Value("\${spring.data.redis.port}")
    private val port: Int
) {

    @Bean(destroyMethod = "shutdown")
    fun redissonClient(): RedissonClient {
        val config: Config = Config()
        config.useSingleServer()
            .setAddress("redis://$host:$port")
            .dnsMonitoringInterval = -1
        return Redisson.create(config)
    }
}
  • 현재 스프링 레디스의 기본 팩토리인 LettuceConnectionFactory를 사용 중이다. Redisson의 경우 LettuceConnectionFactory로 레디스를 사용할 수 있어 따로 팩토리 빈을 만들어주지 않았다.

분산락 실행 코드

분산락을 실행하는 코드를 추상화 하였다.

@Component
class RedisLockExecutor (
    private val redissonClient: RedissonClient
) {
    fun <R> execute(target:Long, task:() -> R): R {
        val lock = redissonClient.getLock(target.toString())

        try {
            val available = lock.tryLock(10, 1, TimeUnit.SECONDS)

            if (!available) {
                throw RedisException(RedisExceptionType.LOCK_TIME_OUT)
            }
            return task()
        } catch (e: InterruptedException) {
            throw RedisException(RedisExceptionType.LOCK_TIME_OUT, e)
        } finally {
            lock.unlock()
        }
    }
}
  • task 람다 파라미터를 이용하여 코드를 공통화 하였다.
  • 10초 까지 락 획득을 시도한다.
  • 락 획득 시 1초 후 락을 반납한다.
@Component
class StudyRoomServiceLockFacade (
    private val studyRoomService: StudyRoomService,
    private val redisLockExecutor: RedisLockExecutor
) {

    fun joinStudyRoom(studyRoomId: Long, userId: Long) {
        redisLockExecutor.execute(studyRoomId) {studyRoomService.joinStudyRoom(studyRoomId, userId)}
    }
}
  • StudyRoomServiceLockFacade클래스를 통해 락을 획득한 유저만이 스터디 방에 가입할 수 있다.

github 스터디방 참여 동시성 제어

profile
오직 나만을 위한 글. 틀린 부분 말씀해 주시면 감사드립니다.

0개의 댓글