이번 글에서는 최상용님의 "재고시스템으로 알아보는 동시성이슈 해결방법"을 정리해 보려고 합니다.
실제로 데이터에 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 단점
락을 걸지 않고, 버전을 이용해서 정합성을 맞추는 방법입니다.
읽어온 데이터와 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
을,
그게 아니라면 Optimistic 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 단점