동시성이슈에 따른 데드락 해결기

tony·2024년 2월 4일
1

이슈해결기

목록 보기
2/3

Context (Business Context)

입찰 경매 시스템을 구현 중에 있다.
하나의 구매입찰 건에 여러 건의 판매 요청을 날려보낸다.
이 때, 동시에 접근하게 되면 충돌이 나지 않을까?

Issue

테스트 코드를 만들어보았다.

예상과 같이 예외가 발생하였다.

Reason

@Transactional
public SellNowResponseDto sellNowProduct(User user, SellNowRequestDto requestDto, Long productId) {

	// 해당상품과 가격에 대한 구매입찰
	Buy buy = buyQueryService.getRecentBuyBidOf(productId, requestDto.price());
	// 쿠폰 조회
	Coupon coupon = getCouponFrom(buy);
	// 새로운 주문
	Order order = saveOrder(buy, user, coupon);

	// 기프티콘 이미지 S3 저장
	String url = s3ImageService.getUrlAfterUpload(requestDto.file());

	// 새로운 기프티콘 저장
	gifticonCommandService.saveGifticon(url, order);
	// 판매에 따른 사용자 포인트 충전
	user.increasePoint(order.getExpectedPrice());

	// 구매입찰 삭제
	commandService.delete(buy);

	// 매퍼를 통해 변환
	return OrderMapper.INSTANCE.toSellNowResponseDto(order);
}

위는 비즈니스를 수행하는 코드인데 -- 리팩토링은 신경쓰지 말자. 시간이 없어서 코드 분리를 못 했다 ㅠ
문제는 구매입찰에 대한 조회 시, 주문처리 이후 주문상태로 처리하는 로직이었다.

조회한 구매입찰은 주문상태 처리를 한다 -- 여기서는 soft delete 처리를 한다.
즉, 조회 이후 상태변경을 한다. 이게 문제이다.
상태변경 처리 중, 다른 스레드에서 조회를 하게 된다면 서로 다른 상태의 데이터를 읽어오게 된다.
따라서 상태변경 기간 동안의 Ex Lock 이 필요하다.

이를 처리하는 것이 PESSIMISTIC_WRITE 이다.

2.2. PESSIMISTIC_WRITE
Any transaction that needs to acquire a lock on data and make changes to it should obtain the PESSIMISTIC_WRITE lock. According to the JPA specification, holding PESSIMISTIC_WRITE lock will prevent other transactions from reading, updating or deleting the data.

Please note that some database systems implement multi-version concurrency control that allows readers to fetch data that has been already blocked.

Pessimistic Locking in JPA [baeldung]

Solution

왜인지, 어떻게 고쳐야할지 알았으니 간단하다 :-)
아래와 같이 조회 쿼리를 수정해주었다.

/**
 * 상품 id와 가격에 대해 가장 먼저 생성된 구매입찰 반환
 *
 * @param productId 상품 id
 * @param price     가격
 * @return 가장 먼저 생성된 구매입찰
 */
@Override
public Optional<Buy> findByProductIdAndPrice(Long productId, Long price) {
	Buy buy = queryFactory
		.selectFrom(QBuy.buy)
		.leftJoin(QBuy.buy.product, product)
		.where(QBuy.buy.product.id.eq(productId), QBuy.buy.price.eq(price))
		.orderBy(QBuy.buy.createdAt.asc())
		.setLockMode(LockModeType.PESSIMISTIC_WRITE) // <-- (수정사항) 조회 시, 배타적 락을 건다
		.fetchFirst();
	return Optional.ofNullable(buy);
}

아래와 같이 쿼리가 수정되어 나갔다. 이를 통해 배타적 락을 사용할 수 있었다.

Further more,,,

  • Do Retry
    • Not sure if @Retryable is valid, because there's some saying it might not release threads to threadpool, since it's reusing the transaction thread of request.


Reference

Concurrency Problem - Update wrong data
Pessimistic Lock in JPA
동시성 문제 해결하기 V2 - 비관적 락(Pessimistic Lock)

profile
내 코드로 세상이 더 나은 방향으로 나아갈 수 있기를

1개의 댓글

comment-user-thumbnail
2024년 10월 20일

감사합니다감사합니다감사합니다감사합니다감사합니다

답글 달기