• 카테고리

    질문 & 답변
  • 세부 분야

    백엔드

  • 해결 여부

    해결됨

낙관적 락 무한루프 도는 이유 공유합니다 !

22.11.09 20:45 작성 조회수 609

5

MySQL을 사용한다면 Isolation Level이 REPEATABLE READ가 기본으로 설정되어 있는데요. 이 때 트랜잭션 안에서 처음 SELECT한 값은 트랜잭션이 끝나기 전까지 몇 번을 다시 SELECT해도 동일한 값으로 읽게 됩니다. 이것 때문에 문제가 발생하는데요...

트랜잭션 안에서 낙관적 락이 진행되도록 구현한 상황에서 쓰레드 100개가 동시에 재고를 감소 시키고, 업데이트에 성공하면 version을 1씩 증가시킨다고 해보겠습니다. DB에는 Stock이 100개 version이 1로 세팅되어 있다고 가정합니다.

@RequiredArgsConstructor
@Service
public class ProductService {
    private final ProductRepository productRepository;

    @Transactional
    public void subtractStockOptimistic(int productId, int quantity){
        int updatedCount = 0;
        while (updatedCount == 0){
            Product product = productRepository.findById(productId);
            product.subtractStock(quantity);
            updatedCount = productRepository.updateStockOptimistic(product);

            if (updatedCount <= 0) { //업데이트에 실패한 경우 50ms 대기
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
  <update id="updateStockOptimistic">
    update test_product
    set stock = #{stock}, version = version + 1
    where id = #{id}
      and version = #{version}
  </update>

쓰레드 100개는 처음에 재고를 읽어서 모두 다음 값을 받아옵니다.

stock : 100, version : 1

제일 빠른 트랜잭션이 업데이트를 성공하고 DB는 다음과 같이 변합니다.

stock: 99, version: 2

이제 나머지 99개의 트랜잭션은 version이 다르므로 모두 업데이트에 실패하게 됩니다.

업데이트에 실패하게 된 트랜잭션들은 재시도를 하게 됩니다.

이 때 DB에서 다시 읽어드린 재고는 stock: 99, version: 2일 것 같지만 실제로는

stock: 100, version: 1 를 읽게 됩니다.

왜냐하면 여전히 하나의 트랜잭션 안에 들어 있고, Isolation Level이 REPEATABLE READ로 처음 읽은 값을 계속 읽게 되기 때문입니다.

따라서 첫 트랜잭션을 제외한 모든 트랜잭션은 무한히 실패하게 됩니다.......

따라서 한 트랜잭션 안에서 업데이트와 재시도 로직이 진행되지 않도록 @Transactional을 메소드에서 떼주시면 정상 동작하게 됩니다.

혹시 정말 Isolation Level 때문인지 확인하고 싶으신 분들은 DB에 Isolation Level을 READ COMMITTED로 바꾸고 테스트를 진행해보시면 @Transactional이 붙어 있어도 정상 동작하는 것을 확인하실 수 있습니다~!

답변 1

답변을 작성해보세요.

0

morib 님 안녕하세요.

무한루프가 발생하는 소스를 github 에 공유해주실 수 있으실까요 ?

하나의 트랜잭션에 묶여있다는 말이 이해가 되지않습니다.

shef님의 프로필

shef

질문자

2022.11.10

원 글에 코드 이미지를 첨부했습니다.

@Transactional이 선언되어 있는 메소드 안에서 업데이트와 재시도를 모두 진행하는 코드입니다.

따라서 하나의 트랜잭션에 묶여있다고 표현했습니다 !

예제에서는 업데이트와 재시도하는 부분을 분리했는데 혹시 한곳으로 몰아놓은 이유가 있으실까요 ?

database 의 Isolation Level 을 변경하는것은 신중하게 선택해야하는 작업이라 여쭤봅니다.

shef님의 프로필

shef

질문자

2022.11.10

한 곳으로 모아놓는 실수를 한 셈입니다 ^^..

이런 실수를 하는 분들이 많은 것 같아서 무한루프에 빠지는 이유를 설명해보았습니다.

Isolation Level을 변경하는 것은 이렇게 변경해서 구현하라는 말보다는, 이렇게 변경해서 테스트해보면 잘되므로 무한루프에 빠진 이유가 REPEATABLE READ 때문이라는 것을 확신시켜 드리기 위함입니다.

 

 

그렇군요 ㅎㅎ

morib 님 좋은공유 감사드립니다 :)

shef님의 프로필

shef

질문자

2022.11.10

넵넵 혹시 글을 보고 DB Isolation Level을 바꾸고 진행하라는 오해가 있을 수 있어서 글을 좀 수정하도록 하겠습니다 ~