카테고리 없음

Redis를 이용한 조회성능 향상시키기

넌 감동란이었어 2024. 8. 31. 01:04

프로젝트를 진행하던 중 아이템 조회 쪽이 자주 방문하게 되는 것을 알게되었다.

설계에 대한 아쉬움이 있긴하지만 아이템 조회는 이미지를 가져오는 공간이기 때문에 매번 db에 접근하기위해 쿼리를 날린다는 것이 성능에 대한 이슈가 있을 것 같았다. 심지어 한번 조회할 때마다 30개의 쿼리가 날라간다.

 

그래서 자주 방문하는 곳을 성능을 생각하며 방문할 수 없을까라는 생각이 들었다.

 

검색해보니 인메모리 데이터 구조 저장소인 Redis를 알게되었고 캐싱개념을 공부해보았다. 

Redis란

Key,Value 구조의 비정형 데이터를 저장하고 관리하기 위한 오픈 소스기반의 비관계형 데이터 베이스 관리 시스템이다. 데이터베이스, 캐시, 메세지, 브로커로 사용되고 인메모리 데이터 구조를 가진 저장소이다.

인메모리란 무엇일까?

In-memory 저장 방식

일반적으로 mysql, oracle 등의 RDB들이 하드디스크에 데이터를 저장하고 관리하는 것과는 달리 레디스는 데이터를 주기억장치인 ram, 즉 In-memory에서 관리한다.

데이터를 저장하고 조회할 때, 하드디스크를 오고 가는 과정을 거치지 않고 메모리 내부에서 처리가 되므로, 기존 RDB보다 훨씬 더 빠른 성능을 발휘할 수 있다. 병목 현상이 발생하지 않는 것이다. 그 외에도 여러 장점들을 가지고 있다.
(한편, RDB도 인메모리 방식을 사용할 수 있는데, mysql/maraidb의 MEMORY 엔진 등의 방식이 그것이다.)

 

캐시 설계

 

캐시 서버는 Look aside cache 패턴이 자주사용되고 Write Back 패턴과 Write Around도 사용된다.

셋 다 알아보자

- Look aside cache

1. 클라이언트가 데이터를 요청

2. 웹서버는 데이터가 존재하는지 Cache 서버에 먼저 확인

3. Cache 서버에 데이터가 있으면 DB에 데이터를 조회하지 않고 Cache 서버에 있는 결과값을 클라이언트에게 바로 반환 (Cache Hit)

4. Cache 서버에 데이터가 없으면 DB에 데이터를 조회하여 Cache 서버에 저장하고 결과값을 클라이언트에게 반환 (Cache Miss)

- Write Back 

1. 웹서버는 모든 데이터를 Cache 서버에 저장

2. Cache 서버에 특정 시간 동안 데이터가 저장됨

3. Cache 서버에 있는 데이터를 DB에 저장

4. DB에 저장된 Cache 서버의 데이터를 삭제

 

* insert 쿼리를 한 번씩 500번 날리는 것보다 insert 쿼리 500개를 붙여서 한 번에 날리는 것이 더 효율적이라는 원리입니다.

* 이 방식은 들어오는 데이터들이 저장되기 전에 메모리 공간에 머무르는데 이때 서버에 장애가 발생하여 다운된다면 데이터가 손실될 수 있다는 단점이 있다.

Write Around 패턴

  • 모든 데이터는 DB에 저장 (캐시를 갱신하지 않음)
  • Cache miss가 발생하는 경우에만 DB와 캐시에도 데이터를 저장
  • 따라서 캐시와 DB 내의 데이터가 다를 수 있음 (데이터 불일치)

 

 

Write Around 패턴은 속도가 빠르지만, cache miss가 발생하기 전에 데이터베이스에 저장된 데이터가 수정되었을 때, 사용자가 조회하는 cache와 데이터베이스 간의 데이터 불일치가 발생하게 된다.

따라서 데이터베이스에 저장된 데이터가 수정, 삭제될 때마다, Cache 또한 삭제하거나 변경해야 하며, Cache의 expire를 짧게 조정하는 식으로 대처해야 한다.

Tip

Write Around 패턴은 주로 Look aside, Read through와 결합해서 사용된다.
데이터가 한 번 쓰여지고, 덜 자주 읽히거나 읽지 않는 상황에서 좋은 성능을 제공한다.

 

 

현 프로젝트에는 Look-aside 전략과 Write Around 패턴을 사용했다.

프로젝트 설정

나는 도커로 레디스를 다운받았다.

sudo docker exec -it docker_redis /bin/bash
root@bc76e4b4bf6a:/data# redis-cli

스프링 레디스 설정

spring:
  redis:
    host: localhost
    port: 6379
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
@Configuration
@EnableCaching
public class RedisCacheConfig {
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory cf) {
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .entryTtl(Duration.ofMinutes(24L)); // 캐쉬 저장 시간 24시간 설정

        return RedisCacheManager
                .RedisCacheManagerBuilder
                .fromConnectionFactory(cf)
                .cacheDefaults(redisCacheConfiguration)
                .build();
    }

}

캐쉬 저장시간을 설정할 수 있다. 아이템 특성상 조회가 빠르게 일어나기때문에 캐쉬 저장시간을 길게 잡아 캐시히트율을 높였다.

 

오직 레디스 캐싱만을 위한 설정이다. 다른 블로그들을 많이 참조해보면 redisTemplate 같은 것들이 있었다. 하지만 그건 좀 더 세부적인 설정을 다룰때 사용하는 것이다. 프로젝트에서는 @어노테이션만 사용했기 때문에 이정도의 설정에서 멈춘다.

 

@Cacheable & @CacheEvict 처리

@Cacheable

읽기 작업을 수행하는 메서드에 사용되는 어노테이션이다.

해당 어노테이션은 메서드의 특정 인자에 대한 메서드 결과값을 캐시 저장소에 key값으로 저장하고 동일한 메서드 결과값은 캐시 저장소에서 가져와 반환한다.

옵션 설명
cacheNames 저장될 캐시 이름을 설정한다. (필자는 상수로 뽑아서 지정했다.)
key Spring 표현식을 사용하여 동적으로 달라지는 메서드의 파라미터 값을 선언한다.
cacheManager  위에서 작성한 캐시 설정 클래스의 메서드 이름을 사용한다.
condition Spring 표현식을 사용해 해당 조건을 만족할 때만 캐시에 저장한다.

 

    @Cacheable(value = "item-cache", key = "'hair:' + #userId")
    public List<CategoryItemRes> getHairItemList(Long userId) {
        return getCategoryItemList(userId, "hair");
    }
    @Cacheable(value = "item-cache", key = "'face:' + #userId")
    public List<CategoryItemRes> getFaceItemList(Long userId) {
        return getCategoryItemList(userId, "face");
    }
    @Cacheable(value = "item-cache", key = "'fashion:' + #userId")
    public List<CategoryItemRes> getFashionItemList(Long userId) {
        return getCategoryItemList(userId, "fashion");
    }
    @Cacheable(value = "item-cache", key = "'background:' + #userId")
    public List<CategoryItemRes> getBackgroundItemList(Long userId) {
        return getCategoryItemList(userId, "background");
    }

카테고리와 userId를 key로 구별했다. 

@CacheEvict

저장된 캐시를 제거할 때 사용된다. 여기서는 키 값을 지정해 해당 키 값만 캐시에서 제거하도록 하였다.

사용된 옵션은 위의 옵션에 대한 설명과 동일하다.

@Caching(evict = {
            @CacheEvict(value = "item-cache", key = "'hair:' + #userId"),
            @CacheEvict(value = "item-cache", key = "'face:' + #userId"),
            @CacheEvict(value = "item-cache", key = "'fashion:' + #userId"),
            @CacheEvict(value = "item-cache", key = "'background:' + #userId")
    })
    public ResponseEntity<?> completeQuest(UserPrincipal userPrincipal, Long id) {
        Character character = characterRepository.findByUserId(userPrincipal.getId()).orElseThrow(CharacterNotFoundException::new);
        Long characterLevel = character.getLevel();
        List<Quest> quests = character.getQuests();

@Caching이라고 한번에 캐싱관련 업무를 해줄 수 있게 도와주는 어노테이션이 있다.

만약 데이터가 변경될 때마다 캐시를 비워주지 않으면 캐시 DB(Redis)와 하드 DB(MySQL)의 데이터가 서로 다른 데이터를 갖고 있어 캐시DB에서 데이터를 꺼내 올 때 변경되기 전인 오래된 정보를 사용하는 데이터 정합성 문제점이 발생한다.

그렇기 때문에 데이터가 변경 될 때마다 캐시를 제거해주는 작업이 중요하다.

결과

JMeter 사용법

https://effortguy.tistory.com/164 

 

 

결과는 40 ㅡ> 9 로 거의 80% 가까이 성능이 좋아졌다.

사실 n+1문제가 걸려 있어 큰 차이가 나는거지만 n+1문제를 해결해도 최소 10%이상은 효과가 났다.

 

 

 

 

 

 

 

출처:
https://hstory0208.tistory.com/entry/Spring-Redis-Redis-cache를-적용해-조회-성능-개선-방법 [< Hyun / Log >:티스토리]
https://velog.io/@tilsong/Redis%EC%9D%98-%EC%A3%BC%EC%9A%94-%EA%B8%B0%EB%8A%A5-1-%EC%9D%B8-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4

 

Redis의 주요 기능 - (1) 인 메모리 데이터베이스

Redis의 주요 기능 중 데이터 베이스로써의 기능을 살펴봅니다!

velog.io

https://hstory0208.tistory.com/entry/Spring-Redis-Redis-cache%EB%A5%BC-%EC%A0%81%EC%9A%A9%ED%95%B4-%EC%A1%B0%ED%9A%8C-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0-%EB%B0%A9%EB%B2%95

 

[Spring + Redis] Redis cache를 적용해 조회 성능 개선 방법

내 프로젝트에서는 사용자별로 사이드바에 찜 수, 장바구니 수들을 카운트 쿼리로 반환된 데이터 수를 view에 뿌려주는데, 이 사이드바는 여러 페이지에서 노출되어 페이지 이동마다 카운트 쿼

hstory0208.tistory.com