실전 프로젝트 3주차. 남은 좌석수를 좀 더 빠른 응답속도로 구현해보고자 Redis를 사용해보려 했다.
예매하기에서 남은 좌석 수 LeftSeats를 감소시키는데 DB에서 여러 쿼리가 나가면서 트랜잭션이 몰리면 응답속도가 너무 느려졌다. 그래서 Redis의 인메모리 저장소를 활용해서 count 만큼 DECR 연산으로 빠르게 관리가 되도롤 해보려고 구현을 시도했다.
@Bean
public RedisTemplate<String, Integer> redisTemplate() {
RedisTemplate<String, Integer> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Integer.class));
return redisTemplate;
}
// 2. 예매하기
@Transactional(isolation = Isolation.READ_COMMITTED)
public Long makeReservation(ReservationRequestDto dto, User user) {
// 먼저 redis 캐시를 조회
Integer leftSeats = redisRepository.findLeftSeatsFromRedis(dto.getTicketInfoId());
if (leftSeats == null) {
// 캐시가 없으면 DB를 통해 남은 좌석 수 차감
decrementLeftSeatInDB(dto);
} else {
// 캐시가 있으면 redis에서 남은 좌석 수 차감
decrementLeftSeatInRedis(dto, leftSeats);
}
Reservation reservation = new Reservation(dto, user);
reservationRepository.saveAndFlush(reservation);
return reservation.getId();
}
// 2-1. redis로 좌석 수 변경
private void decrementLeftSeatInRedis(ReservationRequestDto dto, Integer leftSeats) {
if (leftSeats - dto.getCount() < 0) {
throw new CustomException(ExceptionType.OUT_OF_TICKET_EXCEPTION);
}
redisRepository.decrementLeftSeatInRedis(dto.getTicketInfoId(), dto.getCount());
}
// 2-2. 캐시 없으면 DB로 좌석수 변경
private void decrementLeftSeatInDB(ReservationRequestDto dto) {
TicketInfo ticketInfo = ticketInfoRepository.findById(dto.getTicketInfoId())
.orElseThrow(() -> new CustomException(ExceptionType.NOT_FOUND_TICKET_INFO_EXCEPTION));
if (!ticketInfo.isAvailable()) {
throw new CustomException(ExceptionType.RESERVATION_UNAVAILABLE_EXCEPTION);
}
if (ticketInfo.getLeftSeats() - dto.getCount() < 0) {
throw new CustomException(ExceptionType.OUT_OF_TICKET_EXCEPTION);
}
ticketInfo.minusSeats(dto.getCount());
ticketInfoRepository.save(ticketInfo);
}
캐싱은 수동으로 등록과 삭제가 되도록 했다.
TTL을 사용할 경우 등록된 LS가 삭제되면서 마지막엔 DB에 저장을 해줘야 하는데 그게 구현이 쉽지않아 수동으로 ADMIN이 관리하도록 했다.
// 5. ADMIN. DB에서 남은 좌석수만 가져와서 Redis에 (key-value)형태로 저장
@Transactional
public MessageResponseDto saveLeftSeatsInRedis(Long ticketInfoId) {
TicketInfo ticketInfo = ticketInfoRepository.findById(ticketInfoId)
.orElseThrow(() -> new CustomException(ExceptionType.NOT_FOUND_TICKET_INFO_EXCEPTION));
redisRepository.saveTicketInfoToRedis(ticketInfo);
return new MessageResponseDto(HttpStatus.CREATED, "redis에 성공적으로 저장되었습니다.");
}
// 6. ADMIN. 해당하는 공연의 남은 좌석수 Redis에서 삭제(삭제되기전 모든 캐시 DB에 반영)
@Transactional
public MessageResponseDto deleteLeftSeatsFromRedis(Long ticketInfoId) {
redisRepository.deleteLeftSeatsInRedis(ticketInfoId);
return new MessageResponseDto(HttpStatus.OK, "redis에서 캐시가 성공적으로 삭제되었습니다.");
}
@Component
@RequiredArgsConstructor
public class RedisRepository {
private final RedisTemplate<String, Integer> redisTemplate;
private final TicketInfoRepository ticketInfoRepository;
// 키를 ls1 ls2 이런 패턴으로 "ls+ticketInfoId"로 저장. ls는 소문자.
public void saveTicketInfoToRedis(TicketInfo ticketInfo) {
String key = "ls" + ticketInfo.getId();
redisTemplate.opsForValue().set(key, ticketInfo.getLeftSeats());
}
// 매분 캐시 변경분을 db에 저장
@Scheduled(cron = "0 * * * * *")
public void saveTicketInfoFromRedis(){
Set<String> keys = redisTemplate.keys("ls*");
if (keys.isEmpty() || keys == null) return;
for (String key : keys) {
Integer leftSeatsInRedis = redisTemplate.opsForValue().get(key);
Long ticketInfoId = Long.parseLong(key.substring(2));
TicketInfo ticketInfo = ticketInfoRepository.findById(ticketInfoId).orElseThrow(
() -> new CustomException(ExceptionType.NOT_FOUND_TICKET_INFO_EXCEPTION));
ticketInfo.setLeftSeats(leftSeatsInRedis);
ticketInfoRepository.save(ticketInfo);
}
}
// key로 redis에 조회
public Integer findLeftSeatsFromRedis(Long ticketInfoId){
String key = "ls" + ticketInfoId;
return redisTemplate.opsForValue().get(key);
}
// 값 변경. count만큼 남은좌석수 깎기
public void decrementLeftSeatInRedis(Long ticketInfoId, int count){
String key = "ls" + ticketInfoId;
redisTemplate.opsForValue().decrement(key, count);
}
// 키 삭제. 삭제전 leftSeats 반영
public void deleteLeftSeatsInRedis(Long ticketInfoId){
String key = "ls" + ticketInfoId;
saveTicketInfoFromRedis();
redisTemplate.delete(key);
}
}