-
동시성 제어 syncronized와 @TransactionalBackEnd/스프링 2022. 9. 26. 14:51
서비스에 @Transactional 어노테이션만 붙이면 동시에 자원에 접근하는 스레드들에 레이스 컨디션이 발생했을 경우,
기대와 다르게 동작할 수 있다.
@RequiredArgsConstructor @Service public class StockService { private final StockRepository stockRepository; @Transactional public void decrease(Long id, Long quantity){ Stock stock = stockRepository.findById(id).orElseThrow(); stock.decrease(quantity); stockRepository.saveAndFlush(stock); } }
1. 트랜잭션이 시작되는 메서드에 syncronized 를 메서드에 붙여본다.
@RequiredArgsConstructor @Service public class StockService { private final StockRepository stockRepository; @Transactional public syncronized void decrease(Long id, Long quantity){ Stock stock = stockRepository.findById(id).orElseThrow(); stock.decrease(quantity); stockRepository.saveAndFlush(stock); } }
> @Transactional 은 데이터베이스의 동일한 Entity의 접근에 대해서만 동시에 수정되는 것을 방지한다.
그래서 transaction의 begin과 commit 부분은 syncronized 의 일부가 아니기 때문에
begin을 동시에 한다면 같은 값을 여러 스레드에서 가져가 수정하고 commit 순서에 따라 값이 덮어지게된다.
> @Transaction 을 삭제하고 syncronized 만 남긴다면, 현재 pc에서 동시성 문제가 해결된다. 하지만!
자바의 syncronized 는 하나의 프로세스에서만 보장이 되어, 서버가 여러대라면 데이터 접근을 여러대에서 할 수 있게 된다.
@Test public void multiThreadTest() throws InterruptedException { int threadCount = 100; ExecutorService executorService = Executors.newFixedThreadPool(32); //100개의 요청이 끝날때까지 기다려야해 CountDownLatch countDownLatch = new CountDownLatch(100); for(int i=0;i<threadCount;i++) { executorService.submit(()->{ try{ stockService.decrease(1L,1L); }finally { countDownLatch.countDown(); } }); } countDownLatch.await(); //다른 스레드 수행 완료까지 기다려줌 Stock stock = stockRepository.findById(1L).orElseThrow(); assertEquals(0L,stock.getQuantity()); }
해결방법 1. Transational 격리 레벨은 Serializable 로 올린다.
@Transactional(isolation = Isolation.SERIALIZABLE) public synchronized void decrease(Long id, Long quantity){
해결방법 2. @Transactional 이 있는 StockService 를 호출하는 클래스를 따로 만들어 synchronized 로 감싼다.
@RequiredArgsConstructor @Service public class StockServiceCaller { private final StockService stockService; public synchronized void decrease(Long id, Long quantity){ stockService.decrease(id,quantity); } } @RequiredArgsConstructor @Service public class StockService { private final StockRepository stockRepository; @Transactional public void decrease(Long id, Long quantity){ Stock stock = stockRepository.findById(id).orElseThrow(); stock.decrease(quantity); stockRepository.saveAndFlush(stock); } }
반응형'BackEnd > 스프링' 카테고리의 다른 글
AspectJ와 Spring AOP의 차이점 & Java Agent (0) 2024.12.01 스프링 logback logger 설정 (0) 2022.09.01 단위 테스트의 대상 (0) 2022.08.03 @Profile 대신 @ConditionalOnProperty 을 사용하라 (0) 2022.08.03 ControllerTest - Pageable, SearchDTO (0) 2022.08.02