230417 TIL #61 Redis를 이용해서 공유자원 관리 구현

김춘복·2023년 4월 17일
0

TIL : Today I Learned

목록 보기
61/494

230417 Today I Learned

실전 프로젝트 3주차. 남은 좌석수를 좀 더 빠른 응답속도로 구현해보고자 Redis를 사용해보려 했다.


Redis 구현

예매하기에서 남은 좌석 수 LeftSeats를 감소시키는데 DB에서 여러 쿼리가 나가면서 트랜잭션이 몰리면 응답속도가 너무 느려졌다. 그래서 Redis의 인메모리 저장소를 활용해서 count 만큼 DECR 연산으로 빠르게 관리가 되도롤 해보려고 구현을 시도했다.

  • RedisConfig의 RedisTemplate 설정
    key는 String으로 "ls"+ticketInfoId의 값으로 저장되게, value에는 남은 좌석 수가 Integer 타입으로 들어가서 간단한 key-value String 타입으로 저장되게 했다.
  @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;
  }
  • ReservationService
    LookAside 읽기 전략으로 Redis에 있으면 그 값을 조정하고 없으면 DB로 조정해 예매가 되도록 했다.
  // 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에서 캐시가 성공적으로 삭제되었습니다.");
  }
  • RedisRepository
    인터페이스가 아니라 class로 구현.
    스케쥴링 기능으로 매 분마다 Redis의 LS값을 DB에 동기화 시키도록 했다.
@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);
  }

}

정리

  • 자주 변경되는 값을 이렇게 Redis로 관리하는 것이 맞는가에 대한 의문이 있긴 하지만 일단은 응답속도 개선을 위해 구현을 시도해 보았다. 아직 테스트 결과는 안나왔지만 로직 상 기능은 정상적으로 작동한다. 내일 jmeter로 테스트를 해보고 성능 개선이 있는지 없는지 적어보려한다.
profile
꾸준히 성장하기 위해 매일 log를 남깁니다!

0개의 댓글