TIL

[TIL] Write-Behind 캐싱 구현

dev_ajrqkq 2024. 8. 21. 21:36

 

Write-Behind

 

캐시에만 데이터를 작성하고, 일정 주기로 원본을 갱신하는 방식

쓰기가 잦은 상황에서 데이터베이스의 부하를 줄일 수 있다.

 

예시) 특가 물량, 라이브 등 특정 상황에서 짧은 시간동안 몰리는 상황

 

public void purchase(ItemOrderDto dto) {
        //(1)
        Item item = itemRepository.findById(dto.getItemId())
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
        //(2)
        orderOps.rightPush("orderCache::behind", dto);
        //(3)
        rankOps.incrementScore(
                "soldRanks",
                ItemDto.fromEntity(item),
                1
        );
    }

상품 구매 시 Redis에 데이터를 저장한다.

 

(1) : 해당 아이템이 있는지 확인한다.

(2) : orderCache::behind라는 키를 가진 리스트를 생성하여 redis 리스트의 오른쪽 끝에 dto 객체를 추가한다.

(3) : Double incrementScore(K key, V value, double delta);

key는 Redis Sorted Set을 식별하는 키, 지정된 value 요소의 점수를 delta만큼 증가시킨다.

순위를 보여줄 때는 reverseRange()를 사용하여 점수가 높은 값부터 반환해준다.

 

@Transactional
@Scheduled(fixedRate = 20, timeUnit = TimeUnit.SECONDS) //20초에 한 번씩 동작
public void insertOrders() {
    //(1)
    boolean exists = Optional.ofNullable(orderTemplate.hasKey("orderCache::behind"))
            .orElse(false);
    if (!exists) {
        log.info("no orders in cache");
        return;
    }
    //(2)
    orderTemplate.rename("orderCache::behind", "orderCache::now");
    log.info("saving {} orders to db", orderOps.size("orderCache::now"));
    //(3)
    orderRepository.saveAll(Objects.requireNonNull(orderOps.range("orderCache::now", 0, -1)).stream()
            .map(dto -> ItemOrder.builder()
                    .itemId(dto.getItemId())
                    .count(dto.getCount())
                    .build())
            .toList());
    //(4)
    orderTemplate.delete("orderCache::now");
}

스케쥴러를 만들어 주기적으로 db에 데이터를 넣어주는 작업을 한다.

 

(1) : Redis에 orderCache::behind key가 존재하는 지 확인한다.

(2) : 적재된 주문 처리를 위해 별도로 이름을 변경한다.

다음 데이터는 받지 않고 다른 큐로 넣기 위함. 일종의 락 역할을 한다.

(3) : orderCache::now 리스트의 모든 주문 데이터를 가져와 db에 저장한다.

(4) : db에 저장된 캐시는 삭제하여 메모리 자원을 해제한다.

 

 

그 외) save와 saveAll

한 번의 호출로 여러 엔티티를 처리하여 데이터베이스와의 통신 횟수가 줄어드므로 저장할 엔티티의 양이 많을수록 saveAll이 훨씬 효율적이다