-
mysql을 활용한 동시성 문제 해결 - 2. Optimistic Lock백엔드 관련 강의 공부/동시성 이슈 - 재고시스템 2024. 2. 3. 16:39
2. Optimistic Lock
실제로 lock을 이용하지 않고 버전을 사용해서 정확성을 맞추는 방법
위의 사진처럼 mysql에 version이 존재한다. 만약에 서버1과 서버2가 version이 1인 데이터를 가져간다고 가정하자. 이후에 서버1이 update를 하면서 version도 하나 올려준다. 다음에 서버 2가 version1인 데이터를 가지고 update를 시도하는데, 이 때의 version은 2이기 때문에 update가 실패하게 된다.
수정사항이 실패했기 때문에 application에서 다시 읽은 다음에 로직을 다시 수행한다.
@Entity @Getter public class Stock { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Long productId; private Long quantity; @Version private Long version; ....(생략) }
엔티티에 @Version 어노테이션으로 version을 만든다. 이 때의 @Version 어노테이션은 jakarta의 persistence 어노테이션을 사용해야 한다.
public interface StockRepository extends JpaRepository<Stock, Long> { @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("select s from Stock s where s.id = :id") Stock findByIdWithPessimisticLock(Long id); @Lock(LockModeType.OPTIMISTIC) @Query("select s from Stock s where s.id = :id") Stock findByIdWithOptimisticLock(Long id); }
그 다음 StockRepository의 PessimisticLock 메서드 아래에 OptimisticLock 메서드를 만든다.
이 때는 @Lock 어노테이션에 LockModeType.OPTIMISTIC을 넣어준다.
OptimisticLockStockService.class
@Service @RequiredArgsConstructor public class OptimisticLockStockService { private final StockRepository stockRepository; @Transactional public void decrease(Long id, Long quantity) { Stock stock = stockRepository.findByIdWithOptimisticLock(id); stock.decrease(quantity); stockRepository.save(stock); } }
그 이후에 이전과 같이 decrease를 하는 service를 만들어준다. 이 때, 실패하면 재시작을 해야하기 때문에 facade 패키지를 만들어서 성공할 때까지 재시작하는 함수를 만들어준다.
OptimisticLockStockFacade.class
@Component @RequiredArgsConstructor public class OptimisticLockStockFacade { private final OptimisticLockStockService optimisticLockStockService; public void decrease(Long id, Long quantity) throws InterruptedException { while (true) { try { optimisticLockStockService.decrease(id, quantity); break; } catch (Exception e) { Thread.sleep(50); // 50ms 이후에 재시작 } } } }
실패하면 50ms 이후에 재시작하고, 성공하면 decrease를 한 다음에 break를 한다.
@SpringBootTest public class OptimisticLockStockFacadeTest { @Autowired private OptimisticLockStockFacade optimisticLockStockFacade; @Autowired private StockRepository stockRepository; @BeforeEach public void before() { stockRepository.save(new Stock(1L, 100L)); } @AfterEach public void after() { stockRepository.deleteAll(); } @Test public void 동시에_100개의_요청() throws InterruptedException { int threadCount = 100; ExecutorService executorService = Executors.newFixedThreadPool(32); // Excutors는 비동기로 실행하는 작업을 단순화해서 사용할 수 있게 해주는 자바의 API이다. CountDownLatch latch = new CountDownLatch(threadCount); // CountDownLatch는 다른 thread에서 수행중인 작업이 완료될 때까지 도와주는 클래스 for (int i = 0; i < threadCount; i++) { executorService.submit(() -> { try { optimisticLockStockFacade.decrease(1L, 1L); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { latch.countDown(); } }); } latch.await(); Stock stock = stockRepository.findById(1L).orElseThrow(); // 100 - (1 * 100) = 0; assertEquals(0, stock.getQuantity()); } }
테스트 코드를 만들어서 실행시켜봤더니 성공적으로 실행되었다.
장점
별도의 lock을 잡지 않으므로 PessimisticLock보다 성능 상의 이점이 있다.
단점
update가 실패했을 때 재시도 로직을 개발자가 작성해야하고, 잘 작성해야 한다.
충돌이 빈번하게 일어날 것이라고 예상된다면 PessimisticLock을 사용한다.
충돌이 자주 일어나지 않을 때 추천하는 방법이 이번에 사용한 OptimisticLock이다.'백엔드 관련 강의 공부 > 동시성 이슈 - 재고시스템' 카테고리의 다른 글
redis를 활용한 동시성 문제 해결 - 2. Redisson (0) 2024.02.04 redis를 활용한 동시성 문제 해결 - 1. Lettuce (0) 2024.02.04 mysql을 활용한 동시성 문제 해결 - 3. Named Lock (0) 2024.02.03 mysql을 활용한 동시성 문제 해결 - 1. Pessimistic Lock (0) 2024.02.03 재고 시스템에서 동시성 문제 (0) 2024.02.03