레디스 스핀락을 적용해서 동시성 제어를 진행합니다.
public <T> T runOnLock(Long key, Supplier<T> task) {
while (true) {
if (!lock(key)) {
try {
log.info("락 획득 실패");
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new CustomException(ErrorType.FAILED_TO_ACQUIRE_LOCK);
}
} else {
log.info("락 획득 성공, lock number : {}", key);
break;
}
}
try {
return task.get();
} finally {
// Lock 해제
log.info("락 해제");
unlock(key);
}
}
스핀락을 통해 동시성 제어를 시도했으나, 실패.
10대 주차 가능한 주차장에 20개의 스레드가 동시에 입차를 요청시 15~19대의 차량이 입차에 성공
CPU과부하로 인한 redis 성능 저하로 동시성 처리 오류 → 20개의 스레드밖에 안보내서 아닐 것 같음
더 작은 수의 스레드로 요청을 보내봤으나 (2대의 주차장에 5개의 스레드 동시 요청시 3대 입차) 똑같은 문제가 발생 → 락 획득 실패시 sleep 시간이 짧은가 싶어서 sleep 시간을 동적으로 더 늘려봤으나 여전히 문제가 발생
서비스 레이어에서 바로 스핀락을 사용하게 되면 테스트 코드에 실패함
→ 락 해제 시점과 트랜잭션 커밋 시점의 불일치
서비스 단의 enter 메서드에는 @Transactional 어노테이션이 붙어 있기 때문에 스프링 AOP를 통해 enter 메서드 바깥으로 트랜잭션을 처리하는 프록시가 동작. 반면 락 획득과 해제는 enter 메서드 내부에서 발생하기 때문에 스레드 1과 스레드 2가 경합한다면 스레드 1의 락이 해제되고 트랜잭션 커밋이 되는 사이에 스레드 2가 락을 획득하게 되는 상황 발생. 데이터베이스 상으로 락이 존재하지 않기 때문에 스레드 2는 데이터를 읽어오게 되고, 스레드 1의 변경 내용은 유실됨
→ 락 범위가 트랜잭션 범위보다 크도록 만들어 해결
public <T> T runOnLock(Long key, Supplier<T> task) {
while (true) {
if (!lock(key)) {
try {
log.info("락 획득 실패");
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new CustomException(ErrorType.FAILED_TO_ACQUIRE_LOCK);
}
} else {
log.info("락 획득 성공, lock number : {}", key);
break;
}
}
try {
return task.get();
} finally {
// Lock 해제
unlock(key);
}
}
public CarInResponseDto enter(CarNumRequestDto requestDto, Admin admin) {
return redisLockRepository.runOnLock(
requestDto.getParkId(),
() -> enterLogic(requestDto, admin)
);
}
추가 문제 ->
위와같이 코드를 작성시 enter
내부 메서드 enterLogic
을 실행할때 Transaction
이 실행되지 않고 무시된다. 그 이유는 @Transactional
은 Spring의 AOP를 이용하여 동작하는데 AOP는 프록시를 이용하여 메서드를 호출하는데, enter
메서드에서 enterLogic
메서드를 호출할 때는 인스턴스가 새로 생성되지 않기 때문에 프록시가 생성되지 않고 @Transactional
이 동작하지 않는다.
따라서 enterLogic
메서드를 호출할때 별도의 트랜잭션 핸들러 클래스를 만들어 프록시 호출을 가능하게 만들어서 @Transactional
이 정상 작동 할 수 있도록 해주었다.
public CarInResponseDto enter(CarNumRequestDto requestDto, Admin user) {
if (requestDto.getParkId() == null) {
throw new CustomException(ErrorType.CONTENT_IS_NULL);
}
return redisLockRepository.runOnLock(
requestDto.getParkId(),
()->transactionHandler.runOnWriteTransaction(() -> enterLogic(requestDto, user)));
}