본 글은, 스프링부트 환경에서, 데이터에 동시에 접근하게 되는 경우 트랜잭션 동시성 이슈를 해결하는 방법( 참고강의 )에 대해 설명한다.
1. Synchronized 이용
Java에서 지원해주는 synchronized를 활용하여 한개의 쓰레드만 접근 가능하게 만들어준다.
//@Transcational
public synchronized void decrease(Long id, Long 수량){
Stock stock = stockRepository.findById(id).orElseThrow();
stock.decrease(수량);
stockRepository.saveAndFlush(stock);
}
이때, Spring에서는 @Transactional을 사용하게 되면, 트랜잭션이 종료되기전에 다시 쓰레드(동시성으로 접근)호출하는 경우가 나타날 수 있음.
=> 문제점 : synchronized는 각 Process 안에서만 유효함. 서버가 2대 이상인 경우, 문제생김.
2. DB 기능 사용하기
- 충돌이 빈번하게 일어날것같으면 Pessimistic Lock(비관적 락) 추천.
- 충돌이 빈번하지 않을것같으면 Optimistic Lock(낙관적 락) 추천.
- 상세한 타임아웃과 데이터삽입시 활용하려면 Named Lock 사용.
- Pessimistic Lock : 실제 데이터에 Lock 걸어서 정합성 맞춤. 데드락 주의해야함
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);
}
- Optimistic Lock :버전을 이용하여 정합성을 맞춤. 먼저 데이터를 읽은 후 update 수행시, 읽은 버전이 맞는지 확인. 내가 읽은 버전에서 수정사항이 생긴 경우, application에서 다시 읽은 후 수행.
public interface StockRepository extends JpaRepository<Stock, Long> {
@Lock(LockModeType.OPTIMISTC)//<---이 어노테이션 기능을 활용
@Query("select s from Stock s where s.id= :id")
Stock findByIdWithOptimisticLock(Long id);
}
@Component
@Requiredargsconstructor
public class OptimisticLockStockFacade{
private final OptimisticLockStockService optimisticLockStockService;
public void decrease(Long id, Long quantity){
while(true){//실패할때 재시도를 계속 해야함.
try{
optimisticLockStockService.decrease(id, quantity);
break;//<--성공했다면 while문을 나오기
}catch (Exception e){
Thread.sleep(50);//<--실패하면 50ms동안 쉬었다가 다시 시도
}
}
}
}
장점) Lock을 잡지 않으므로 성능상 이점이 있다.
단점) 충돌이 많이 일어날 시(=update 많은 경우), 재시도 로직(개발자가 일일이 작성해야된다는 단점 포함)을 타야하므로 오래걸릴 수 있음.
- Named Lock : 이름을 가진 metadata locking. 이름을 가진 lock 획득 후, 해제할때까지 다른 세션은 이 lock을 획득 할 수 없음.(*주의할점 - 트랜잭션이 종료될때 lock이 자동 종료되지 않으므로 별도의 명령어로 해제하거나 선점 시간이 끝나야 해제됨)
public interface StockRepository extends JpaRepository<Stock, Long> {
@Lock(value = "select get_lock(:key, 3000)", nativeQuery = true)
void getLock(String key);
@Query(value = "select release_lock(:key)", nateiveQuery = true)
void releaseLock(String key);
}
@Component
@Requiredargsconstructor
public class NamedLockStockFacade{
private final LockRepository lockRepository;
private final StockService stockService;
@Transactional
public void decrease(Long id, Long quantity){
try{
lockRepository.getLock(id.toString());
stockService.decrease(id,quantity);
}finally{
lockRepository.releaseLock(id.toString());
}
}
}
장점) Timeout 손쉽게 구현. 데이터 삽입 시, 정합성을 맞추는 데에 사용할 수 있음.
단점) 실제로 사용시, 구현 방법이 복잡할 수 있음.(트랜잭션, Lock해제 주의해서 사용해야함)
3. Redis 이용하기
=> 실무에서는 재시도가 필요하지 않은 Lock은 Lettuce, 필요한 Lock은 Redission을 활용함.
- Lettuce
장점 | 단점 |
구현 간단 | spin lock 방식이기때문에 동시에 많은 쓰레드가 Lock 획득 대기상태라면, redis에 부하가 갈 수 있다 |
Spring data redis 사용시, 별도 라이브러리 사용하지 않아도됨 |
- setnx 명령어 활용하여 분산락 구현
- spin lock 방식
- Redission
장점 | 단점 |
Lock 획득 재시도를 기본으로 제공 | lock을 라이브러리 차원에서 제공해주기 떄문에 사용법 공부가 필요함 |
pub-sub 방식 구현으로 lettuce와 비교했을 때, redis에 부하가 덜 간다 |
- pub-sub 기반으로 Lock 구현 제공
Redis 부분 소스 부분은 이 링크 참고
동시성 해결 : MySql vs Redis
MySql | Redis |
이미 Mysql 사용시, 별도 비용없이 사용 가능 | 활용중인 Redis가 없다면 별도 구축비용 및 인프라 관리비용 발생 |
어느정도의 트래픽까지는 문제없이 활용 가능 | Mysql보다 성능이 좋음 |
Redis보다 성능이 좋지 않음 |
'Spring > SpringBoot' 카테고리의 다른 글
멀티 모듈 프로젝트 구조와 설계(#SpringBoot#Gradle) (1) | 2024.02.12 |
---|---|
Nexus/FW버전/OpenShift/베어메탈/Pod(파드) (0) | 2023.02.27 |
Redis를 사용해서 스프링부트 캐시 (0) | 2022.06.01 |
SpringBoot CORS에러 해결하기 (0) | 2022.05.01 |
Swagger UI를 SpringBoot 연결하기 (0) | 2022.05.01 |