BackEnd/스프링

동시성 제어 syncronized와 @Transactional

ssseung 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);
    }
}

 

반응형