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을 보호해서 정확한 발급을 보장 할 수 있지만, 해당 부분을 한 쓰레드씩 사용하게 만들기 때문에 멀티쓰레딩의 효율이 줄고, 처리량이 줄어든다.
필요한 부분에만 최소한으로 사용하도록 하자.
'토이 프로젝트' 카테고리의 다른 글
Kafka: The coordinator is not available. 에러 (0) | 2024.12.06 |
---|---|
Reactive Kafka를 사용해 Email notification 서버에서 메일 보내기 (0) | 2024.11.28 |
Redis Sentinel 설정 (0) | 2024.11.09 |
Redis의 pub-sub을 사용한 SSE notification 서버 만들기 (1) | 2024.11.09 |
Redis를 사용하여 DB Cache 구현하기 (1) | 2024.11.07 |