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);
}
}
반응형