반응형

Webflux 토이프로젝트로 500개의 쿠폰을 발급하는 서비스를 만들고, 부하테스트를 하고 있었다.

 

분명 발급 제한 수와 실제 발급 수가 200으로 같지만

 

해당 쿠폰으로 무려 40000개가 발급이 되어있었다.

 

운영체제와 데이터베이스를 공부할 때 나오는 critical section 문제이다.

 

아래와 같이 한 유저가 조회하고 수량을 증가하여 저장하기 전에, 다른 유저가 중간에 끼어들어 조회하면 비록 2명 이상이 조회하더라도 수량은 1만 증가하게 되는 것이다.

 

그렇기 때문에 조회하고 저장하는 그 부분을 critical section으로 보호하여, 다른 쓰레드의 개입이 없도록 해야한다.

 

이 방법을 해결하기 위해 외부 동시성 제어 메커니즘을 사용해보려 한다.

WebFlux를 사용하기에 RedissonReactiveClient를 사용해보겠다.

 

우선 Bean을 만들어주자.

gradle에 다음 의존성을 추가해준다.

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

 

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

    @Bean
    fun redissonClient(): RedissonClient {
        val config = Config()
        val serverConfig: SingleServerConfig = config.useSingleServer()
        serverConfig.address = "redis://${host}:${port}"
        return Redisson.create(config)
    }

    @Bean
    fun redissonReactiveClient(): RedissonReactiveClient = redissonClient().reactive()
}

 

host, port를 넣어서 RedissonClient를 만들어주고, 그걸 바탕으로 redissonReactiveClient도 만들어준다.

redissonClient().reactive()로 reactive하게 만들어 줄 수 있다.

Redis 설정을 해주었더라도, 여기에도 redis 정보를 넣어주어야 한다.

 

이제 사용하는 방법이다.

@Service
class CouponIssueService(
    private val couponRepository: CouponRepository,
    private val couponIssueRepository: CouponIssueRepository,
    private val redissonReactiveClient: RedissonReactiveClient
) {

    val lock: RLockReactive = redissonReactiveClient.getLock("coupon_lock")

    @Transactional
    suspend fun issueByLock(couponId: String, userId: String): CouponIssue? {
        val couponObjectId = ObjectId(couponId)
        val userObjectId = ObjectId(userId)


        return lock.lock(10, TimeUnit.SECONDS)
            .then(Mono.defer {
                findCoupon(couponObjectId)
                    .flatMap { coupon ->
                        coupon.issue()
                        couponRepository.save(coupon)
                            .then(saveCouponIssue(couponId = couponObjectId, userId = userObjectId))
                    }
                    .doFinally {
                        lock.unlock()
                    }
            }).awaitSingle()
    }
}

 

우선 해당 부분을 lock 할 수 있는 key를 만들어야 한다.

getLock()으로 해당 부분을 lock, unlock 할 수 있는 키를 만들 수 있다.

 

lock 하는 방법은 해당 key에서 lock 메서드를 호출하는 것이다.

lock().then()으로 lock 한 후 실행할 작업들을 명시 할 수 있다.

 

작업이 다 되었다면 .doFinally()를 사용하여 unlock해서 해당 부분을 풀어주고 나와야 한다.

 

여기서 중요한 점은 lock하는 쓰레드와 unlock하는 쓰레드가 같아야 한다는 것이다.

코루틴이나 리액티브 프로그래밍 등으로 쓰레드가 바뀐다면 에러가 발생 할 것이다.

 

정확한 개수로 발급하는 지 확인해보도록 하자.

 

 

목표한 수량은 200개였고

 

발급된 수량을 확인해보니

정확하게 200개가 발급된 것을 볼 수 있다.

 

물론 이렇게하면 critical section을 보호해서 정확한 발급을 보장 할 수 있지만, 해당 부분을 한 쓰레드씩 사용하게 만들기 때문에 멀티쓰레딩의 효율이 줄고, 처리량이 줄어든다.

필요한 부분에만 최소한으로 사용하도록 하자.

+ Recent posts