<aside> ❗ 락을 적용시키고 재고감소는 락 내부에서 했지만, 실제 재고의 update쿼리가 락 외부에서 날아가던 문제가 발생하였습니다.
</aside>
문제가 발생했던 코드
/* OrderService.java */
@Transactional
public void orderBasketProducts(String orderRequest) throws JsonProcessingException {
.
.
.
for (OrderProduct orderProduct : orderProductList) {
Long productId = orderProduct.getProduct().getId();
RLock lock = redissonClient.getLock("product" + productId); //상품ID별로 락 획득
try {
boolean available = lock.tryLock(10, 5, TimeUnit.SECONDS); // 락 획득 시도
if (!available) {
log.error("주문 시도 중 lock 획득 실패");
continue;
}
productService.decrease(productId, orderProduct.getCount()); // 재고를 감소시키는 부분
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (IllegalArgumentException e) {
log.error("재고 부족으로 구매 실패");
} finally {
lock.unlock(); // 락 해제
}
}
}
문제는 락과 트랜잭션의 범위와 @Transactional 의 동작원리에 있었습니다.
@Transactional 어노테이션이 붙어있으면 기본적으로 해당 서비스(OrderService)에서 로직을 호출하지 않고, ProductService를 필드 변수로 주입받은 다른 객체에서 OrderService의 메소드를 호출해 작업을 수행하게 됩니다.
/* ProductService를 필드변수로 주입 받은 다른 객체*/
.
.
.
// 트랜잭션 생성
productService.decrease();
//<- 이 사이에 다른 쓰레드가 락을 점유하고 수정되기 이전 값을 변경시킴
// 트랜잭션 커밋
.
.
.
이 과정에서 해당 가짜 객체는 대략적으로 위와같은 방식으로 트랜잭션을 열고, 서비스로직을 수행하고, 해당 변경사항을 반영하는데요.
변경사항이 반영되기 이전에 락이 풀리고 다른 쓰레드가 락을 점유하게 되어 데이터의 동시성 문제가 발생하였습니다.
/* OrderService.java */
public void orderBasketProducts(String orderRequest) throws JsonProcessingException {
.
.
.
for (OrderProduct orderProduct : orderProductList) {
Long productId = orderProduct.getProduct().getId();
RLock lock = redissonClient.getLock("product" + productId); //상품ID별로 락 획득
try {
boolean available = lock.tryLock(10, 5, TimeUnit.SECONDS);
if (!available) {
log.error("주문 시도 중 lock 획득 실패")
continue;
}
productService.decrease(productId, orderProduct.getCount());
// 메소드가 끝나고 트랜잭션이 commit된 후 finally에서 unlock();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (IllegalArgumentException e) {
log.error("재고 부족으로 구매 실패");
} finally {
lock.unlock();
}
}
}
OrderService의 주문 메소드 자체에 걸었던 @Transactional 어노테이션을 제거하고, 아래와 같이 락 내부 재고를 감소시키는 ProductService.decrease()로 트랜잭션 범위를 좁혀 락 내부에서 update쿼리가 날아갈 수 있도록 개선하였습니다.