예매를 생성하는 api 기능에 동일한 1000개의 요청을 동시에 보낼 시
이처럼 예외처리가 되어있지만
10번의 성공과 990번의 500 에러를 반환한다.
db를 확인해보면 동일한 좌석에 대해 10건의 예매가 생성되어있다.
200코드로 성공한 10건의 예매는 동시성 문제로 예매가 성성되었고,
500코드로 실패한 990건의 요청은 예외처리 구간에서 db 조회 시
Optional<Booking> findByScheduleIdAndSeat(Long scheduleId, String seat);
이처럼 Booking 타입으로 조회가 되어야하지만 10건의 List가 조회되어 발생한 에러이다.
동시성을 해결하는 간단한 방법으로 Service 코드에 적용되어있는 Transactional 어노테이션을 활용하는 것이다.
같은 조건으로 1000번의 요청을 보낼 시
테스트 결과를 확인하기 위해 200 성공 코드와 400 잘못된 요청 코드는 failed가 아니도록(check하도록) 하였다.
이처럼 983개의 요청은 성공(200, 400)하고, 17건의 요청은 동시성 문제로 failed가 된 것을 확인할 수 있다.
더 확실하게 동시성 문제를 해결하고, 속도 개선을 위해 Redis를 적용해 테스트해보기로 했다.
의존성 추가
(다른 client 모듈에서 redis 모듈을 의존하고 있기 때문에 redis의 build.gradle을 사용하기 위해 implementation이 아니라 api로)
RedissonConfig 클래스를 작성
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://localhost:6379")
.setPassword("systempass");
return Redisson.create(config); // RedissonClient 인스턴스 생성
}
}
BookingService의 createBooking 메서드에 Lock 설정을 해준다.
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class BookingService {
private final BookingRepository bookingRepository;
private final BookingMapper bookingMapper;
private final UserClient userClient;
private final ConcertClient concertClient;
private final PaymentService paymentService;
private final RedissonClient redissonClient;
@Transactional(isolation = Isolation.SERIALIZABLE)
public List<CreateBookingRes> createBooking(Long userId, CreateBookingReq createBookingReq) {
// 분산 락 이름 정의 (예약 스케줄 ID 기반)
String lockKey = "bookingLock:" + createBookingReq.scheduleId();
RLock lock = redissonClient.getLock(lockKey); // 락 객체 생성
// 락을 획득하고, 예외 발생 시 락 해제
try {
lock.lock(); // 락 획득
// TODO : 대기열
if (!userId.equals(createBookingReq.userId())) {
throw new GlobalException(ErrorCase.NOT_AUTHORIZED);
}
// 이미 예약된 좌석인지 확인
List<Booking> bookings = new ArrayList<>();
createBookingReq.seat().forEach(seat -> {
bookingRepository.findByScheduleIdAndSeat(createBookingReq.scheduleId(), seat)
.ifPresent(booking -> {
throw new GlobalException(ErrorCase.ALREADY_BOOKED_SEAT);
});
Booking booking = createBookingReq.toEntity(seat);
bookings.add(booking);
});
// 예매 정보 저장
bookingRepository.saveAll(bookings);
return bookings.stream().map(bookingMapper::toCreateBookingRes).toList();
} finally {
// 락 해제
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
// ...
}
RLock lock = redissonClient.getLock(lockKey);
를 통해 특정 자원(여기서는 예약 스케줄 ID에 기반한 락)을 위한 락 객체를 생성한다.전부 다 성공
DB에는 당연히 하나의 데이터만 저장되어있다.
1000건의 요청으로는 뭔가 찝찝한 느낌이 들어 요청을 5000건으로 늘려서 다시 테스트해보기로 했다.
@Transactional(isolation = Isolation.SERIALIZABLE)만 적용한 테스트
분산 락을 적용한 테스트
order-service의 ComponentConfig 클래스에
@Configuration
@ComponentScan(basePackages = {
"com.fortickets.exception",
"com.fortickets.jpa",
"com.fortickets.security",
"com.fortickets.redis"
})
public class ComponentConfig {
}
이처럼 레디스 스캔 범위를 지정해줘야 order-service에서 Redis를 사용할 수 있다.
Redis 적용 후 order-service application 실행 시 Redis에 설정된 비밀번호를 찾지 못하는 경우
터미널 명령어로 CLI에 접속
redis-cli -h localhost -p 6379 -a systempass