ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 동시성 제어 syncronized와 @Transactional
    BackEnd/스프링 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);
        }
    }

     

    반응형
Designed by Tistory.