[ToyProject] 동시성 문제 해결에 대한 고민
현재 프로젝트에서는 동시성 문제를 해결하기 위해 해당 트랜잭션에 비관적 락을 걸어버린다.
비관적 락을 거는 것 까지는 좋다. 복잡한 어플리케이션이 아니고 시간이 오래걸리는 요청, 혹은 tps가 높지 않은 서비스의 경우에는 비관적 락을 걸더라도 크게 성능상의 문제가 없을거라고 생각한다.
그러나 문제가 있다.
해당 프로젝트는 불특정 다수의 동시 접근에 대해 빠르고 문제 없는 서비스를 구현할 수 있도록 진행되고 있기에 비관적 락을 통해 트랜잭션 접근을 막아버리는건 큰 문제가 있다.
예를들어, 100명의 사용자가 상품 구매에 동시적으로 접근한다고 가정해보자.
먼저 한 쓰레드가 락을 획득하게 되면 나머지 99명의 사용자의 요청은 해당 사용자의 요청이 끝날때까지 기다려야하고, 그걸 99번이나 진행하게될것이다..
위의 로그 추적기에 나와있다싶이 트랜잭션 락을 얻은 이후에도 실행되는 로직이 상당 수 있다.
그렇기 때문에, 동시접근이 많아지면 많아질수록 속도는 느려질 수 밖에 없다. 정합성은 정확하겠지만 말이다.
@Transactional
public GetOrderDto makeOrder(Item item, Long memberId, long quantity) {
Member member = memberRepository.findById(memberId).orElseThrow();
stockService.decrease(item.getStock().getId(), quantity);
Order order = orderRepository.save(Order
.builder()
.member(member)
.item(item)
.build());
return orderRepository.findOrder(order.getId());
}
해당 구매 서비스에서는 DB에 접근하여 재고 데이터를 얻어온다. 컴퓨터의 주 메모에 저장된 데이터를 가져오는 방식으로, 인메모리 데이터베이스에 비해 느리다는 단점이 있다.
인메모리 DB를 사용해서 물리 디스크에 직접 접근하기보다, 캐싱된 데이터를 가져와서 처리해주는게 어떨까?
대표적인 인메모리 데이터베이스 Redis는 싱글 쓰레드 구조이기 때문에, 분산 락 등을 사용하게 되면 레이스 컨디션 문제도 해결되고, 속도 또한 기존 방식보다 빨리질 것으로 예상된다.
또한, 레플리케이션이나 샤딩 등을 통해 스케일아웃으로 확장하더라도 동시성이 보장되기에, 대규모 서비스 처리에 굉장히 적합하다고 할 수 있다.
@Test
void 주문_생성_동시성_테스트_성공() throws InterruptedException {
long beforeStock = itemRepository.findById(item.getId()).orElseThrow().getQuantity();
int threadCount = 1000;
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
executorService.execute(() -> {
try {
orderService.makeOrder(item, member.getId(), 1);
}
finally {
latch.countDown();
}
}
);
}
latch.await();
Assertions.assertThat(beforeStock - threadCount).isZero();
Assertions.assertThat(orderRepository.findAll().size()).isEqualTo(threadCount);
}
해당 테스트에 대한 응답시간이 총 8545 ms가 걸렸는데, 개선함으로써 얼마나 줄어드는지 확인해보려 한다.
해결 방식으로는 redis를 사용할 예정이다.