개발을 하다보면 데이터 정합성이 굉장히 중요한경우들이 발생한다.
간단한예로는 1인당 구매수량 제한이 걸려있는 상황이 있을 수 있다.
1인당 상품 1개밖에 구매하지 못한다고 했을때 2개이상을 구매하게 되면 절대 안된다.
우리는 아래와 같은 코드를 작성할 수 있을 것이다.
fun order(userId: Long, productId: Long) {
val alreadyBuy = orderRepository.findByUserId(userId, productId)
if(alreadyBuy) {
throw AlreadyBuyException("$userId is already buy")
}
orderRepository.save(userId, productId)
}
언뜻 보기에는 위코드에는 전혀 문제점이 없어보인다.
위 코드에 어떤 문제점이 있는지 생각해보자
10:00 에 request-1 구매내역을 조회했다.
10:01 에 request-2 구매내역을 조회했다.
10:02 에 request-1 구매를 생성한다.
10:02 에 reuqest-2 구매를 생성한다.
위와 같은 상황이 발생한다면 1인당 여러개의 상품을 구매할 수 있게된다.
이럴때 어떻게하면 1인당 1개만 구매할 수 있도록 할 수 있을까 ?
가장 간단하게 생각할 수 있는것은 synchronized 일것이다.
val lock = Any()
fun order(userId: Long, productId: Long) {
synchronized(lock) {
val alreadyBuy = orderRepository.findByUserId(userId, productId)
if(alreadyBuy) {
throw AlreadyBuyException("$userId is already buy")
}
orderRepository.save(userId, productId)
}
}
이 방법은 서버가 1대일때는 별다른 문제가 발생하지 않을 것이다.
하지만 서버가 2대 이상되어도 문제가 발생하지 않을까 ?
여러서버가 있다면 synchronized 가 없을때의 문제점이 재현될 것이다.
그렇다면 이럴땐 어떻게 해결할 수 있을까 ?
필자는 redis 로 lock 을 관리하는 방법을 통해 문제를 해결하고자한다.
redis 는 싱글스레드이기때문에 여러서버에서 동시에 접근하여도 순차적인 처리를 보장해준다.
즉, 동시성을 보장해준다.
또한 빠른 성능이 이미 검증되어 있기때문에 서비스에 문제가 없을거라고 생각한다.
필자는 아래와 같은 프로세스로 동시성을 보장하고자 했다.
- 진입하면서 lock 얻기 시도
- 얻기실패시 throw
- 주문내역이 있는지 조회
- 이미 존재한다면 throw
- 주문저장
- lock 해제
Redis 의 setnx 명령어를 이용하여 lock 을 관리한다. (https://redis.io/commands/setnx/)
setnx 는 set if not exists 의 약어로 key 가 없을때만 set 을 하는 특징을 가지고 있는 명령어이다.
값이 set 됐다면 1을 set 되지 않았다면 0 을 반환하는 특성을 사용하여 lock 획득 여부를 결정한다.
fun lock(key: String): Boolean {
return orderRedisTemplate
.opsForValue()
.setIfAbsent(key, true) ?: false
}
전체적인 흐름을 코드로 본다면 아래와같이 작성될 수 있다.
fun order(
id: String,
userId: Long,
) {
val availableLock = orderLockRedisRepository.lock(id)
if (!availableLock) {
throw IllegalArgumentException("$id already in use")
}
val alreadyBuy = orderRepository.existsBuyHistory(userId, id)
if (alreadyBuy) {
throw IllegalArgumentException("$userId is already buy")
}
orderRepository.order(id, userId)
orderLockRedisRepository.unlock(id)
}
이 예제에서는 굉장히 많은 부분을 축약했다.
첫째로 주문을 할때 상품의 재고를 전혀 고려하지 않았다.
상품의 재고가 1개 남았을 때, 유저 2명이 동시에 주문한다면 재고가 -1이 되는 현상이 발생될 수 있다.
이런경우에는 상품의 재고에도 관리를 해주어야하며, 이럴때는 lock 획득 실패시 실패처리를 하는것이 아니라 잡혀있는 lock 이 끝나면 lock 획득시도를 다시 하는 로직이 추가되어야 한다.
둘째로 redis 의 클러스가 1대만 있다고 가정했습니다.
이때는 redis 가 single points of failure 가 될 수 있습니다.
이런 문제점을 어떻게 처리할지 고려가 되어야합니다.
이글을 읽는 여러분께서는 위의 상황에서는 어떤식으로 처리를 할지 생각을 해보신다면 좋겠습니다.
'개발 > 시스템 설계' 카테고리의 다른 글
선착순 쿠폰 이벤트는 어떻게 구현을 해야할까 ? (1) (0) | 2021.11.09 |
---|
댓글