(2) 동시성 이슈 - MySQL Lock

alsdl0629·2023년 12월 14일
0

동시성 이슈

목록 보기
2/4
post-thumbnail

이번 글에서는 최상용님의 "재고시스템으로 알아보는 동시성이슈 해결방법"을 정리해 보려고 합니다.

실습한 레포지토리 👈

MySQL에서 지원하는 Lock으로 문제 해결

Pessimistic Lock

실제로 데이터에 Lock을 걸어 정합성을 맞추는 방법입니다.

다른 트랜잭션에서 lock이 해제되기 전까지 데이터를 가져갈 수 없게 됩니다.

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select s from Stock s where s.id = :id")
Stock findByIdWithPessimisticLock(Long id);
@Transactional
public void decrease(Long id, Long quantity) {
    Stock stock = stockRepository.findByIdWithPessimisticLock(id);

    stock.decrease(quantity);

   stockRepository.save(stock);
}

Spring Data Jpa에서는 @Lock을 통해 Pessimistic Lock을 구현할 수 있습니다.


쿼리에 for update가 붙는데 락을 걸고, 데이터를 가져오는 부분입니다.

Pessimistic Lock 장점

  • 충돌이 빈번하게 일어날 때는 Optimistic Lock보다 성능이 좋습니다.
  • 락을 통해 업데이트를 제어하기 때문에 데이터 정합성이 보장됩니다.

Pessimistic Lock 단점

  • 락을 잡기 때문에 성능 감소가 있을 수 있습니다.

Optimistic Lock

락을 걸지 않고, 버전을 이용해서 정합성을 맞추는 방법입니다.

읽어온 데이터와 db에 있는 데이터의 버전이 같으면 업데이트를 합니다.
버전이 다르면 어플리케이션단에서 다시 읽은 후 업데이트를 합니다.

@Entity
public class Stock {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long productId;

    private Long quantity;

    // Optimistic Lock을 사용하기 위해 추가
    @Version
    private Long version;
    
    ...
}
public interface StockRepository extends JpaRepository<Stock, Long> {
	@Lock(LockModeType.OPTIMISTIC)
    @Query("select s from Stock s where s.id = :id")
    Stock findByIdWithOptimisticLock(Long id);
}
// 어플리케이션단에서 다시 처리하는 부분
public void decrease(Long id, Long quantity) throws InterruptedException {
    while (true) {
        try {
            optimisticLockStockService.decrease(id, quantity);

            // 정상적으로 업데이트가 된다면 while문을 빠져나옴
            break;
        } catch (Exception e) {
            // 수량 감소에 실패하면 50ms 후에 재시도
            Thread.sleep(50);
        }
    }
}

Optimistic Lock 장점

  • 락을 잡지 않으므로 Pessimistic Lock보다 성능상 이점이 있습니다.

Optimistic Lock 단점

  • 업데이트 실패 시 개발자가 재시도 로직을 직접 작성해야 하는 번거로움이 있습니다.
  • 실패했을 때 재시도하는 로직이 있어서 오래 걸립니다.

Pessimistic Lock VS Optimistic Lock

충돌이 빈번하게 일어나거나 예상된다면 Pessimistic Lock을,
그게 아니라면 Optimistic Lock을 사용한다고 합니다.


Named Lock

이름을 가진 Lock을 획득한 후 해제할 때까지 다른 세션에서 Lock을 획득할 수 없습니다.

주의할 점으로는 트랜잭션이 종료될 때 Lock이 자동으로 해제되지 않기 때문에 직접 해제를 하거나 선점시간이 끝나야 해제됩니다.

Named Lock은 주로 분산락을 구현할 때 사용합니다.

분산락이란?
여러서버에서 공유된 데이터를 제어하기 위해 사용하는 기술입니다.



MySQL에서는 GET_LOCK()을 통해 Named Lock을 획득할 수 있고,
REALSE_LOCK()통해 Named Lock을 해제할 수 있습니다.

Named Lock은 Stock에는 Lock을 걸지 않고, 별도의 공간에 Lock을 겁니다.

public interface LockRepository extends JpaRepository<Stock, Long> {

	// 최대 3초동안 기다린다(타임아웃 설정)
    @Query(value = "select get_lock(:key, 3000)", nativeQuery = true)
    void getLock(String key);

    @Query(value = "select  release_lock(:key)", nativeQuery = true)
    void releaseLock(String key);
}

실무에서는 별도의 JDBC를 사용한다고 합니다.
그리고 DataSource를 공유해서 사용하면 커넥션 풀이 부족할 수 있기 때문에
다른 서비스의 영향을 끼치지 않기 위해 DataSource도 분리한다고 합니다.

spring:
  datasource:
    hikari:
      maximum-pool-size: 40

강의에서는 같은 DataSource를 사용하기 때문에 커넥션 풀을 늘렸습니다.

// 로직 전후로 락 획득, 해체
@Transactional
public void decrease(Long id, Long quantity) {
    try {
        // 락 획득
        lockRepository.getLock(id.toString());

        // 락 획득 후 재고 감소 로직 실행
        stockService.decrease(id, quantity);
    } finally {
        /**
         * 모든 로직이 종료되면 락 해제
         * 예외가 발생하면 락을 해제해 줘야하기 때문에 finally 사용
         */
        lockRepository.releaseLock(id.toString());
    }
}
@Service
public class StockService {
    ...
    // NamedLockStockFacade의 트랜잭션과 별도로 실행되야 함
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void decrease(Long id, Long quantity) {
        // Stokc 조회
        Stock stock = stockRepository.findById(id).orElseThrow();

        // 재고 감소
        stock.decrease(quantity);
    }
}

Named Lock 장점

  • Pessimistic Lock은 타임아웃을 구현하기 힘들지만,
    Named Lock은 타임아웃을 쉽게 구현할 수 있습니다.

Named Lock 단점

  • 트랜잭션 종료 시에 락 해제, 세션 관리를 해줘야하기 때문에 주의 해야하고, 구현 방법이 복잡할 수 있습니다.
profile
인풋보다 아웃풋

0개의 댓글