아래 코드를 보면, 쿠폰을 발행하는 issue 메서드가 있다.
먼저 쿠폰을 조회한 다음, 쿠폰의 발급 갯수를 ++1 해줄 것이다.
그런 다음, coupon id + user id를 함께 저장해주는 coupon issue 내역을 저장한다.
즉, 쿠폰 발행의 issue 메서드는 DB의 관점에서 SELECT, UPDATE, INSERT가 모두 발생하는 행위이다.
@Transactional
public void issue(long couponId, long userId) {
Coupon coupon = findCoupon(couponId);
coupon.issue();
saveCouponIssue(couponId, userId);
}
findCoupon(couponId) 메서드를 더 들여다보면, 쿠폰을 조회하는 query가 다음과 같이 작성되어 있다.
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT c FROM Coupon c WHERE c.id = :id")
Optional<Coupon> findCouponWithLock(@Param("id") Long id);
여기서 핵심은 SELECT와 PESSIMISTIC_WRITE이다.
데이터 조회 시점에서 Row Lock을 걸었다는 것이고, 이 락은 배타적 락으로서, 다른 트랜잭션에서의 읽기와 쓰기 모두를 제한하는 X-Lock(Exclusive Lock)이다.
이것은 메서드 호출 시, 일어나는 일과 발생 가능한 상황을 살펴보아야 답할 수 있는 질문이다.
메서드 호출 시, 크게 다음과 같은 일이 발생한다.
즉, 해당 row만 조회하는 것이고 index를 통해 조회하거나, 범위를 조회하는 일은 하지 않는다. 그렇기 때문에 Row Lock만 하면 되고, Record Lock, Gap Lock은 하지 않아도 된다.
조회 시, 비관적 락인 X-Lock을 적용하는 것으로도 데이터 일관성을 해치는 동시성 이슈는 해결되었다. 하지만 문제는 MySQL이 뚜렷한 병목 지점이 된다는 것이다. MySQL이 병목 구간이 될 확률을 낮추기 위해서는 DB 서버의 스케일업 전략을 취하거나 혹은 DB를 분산해야하는데, DB 분산은 관리도가 꽤 높은 작업이다. 그렇기 때문에 조금 더 유지보수가 용이한 방법으로 동시성 이슈 해결 방법을 전환할 필요가 있다.
RPS : 138, Average Response Time : 6.5 secs

CPU usage : 74%
