본문으로 바로가기

[SPRING] Spring Redis Cache

category SPRING/기본 상식 2021. 7. 5. 10:41

Redis 란?

Key-Value 형태를 띄고 있는 In Memory 기반의 NoSQL DBMS이다.

 

최근에 실무에서도 50만건의 데이터를 추출하기 위해 Redis를 사용할 일이 있었다.

기존 RDMS의 조회 시 7분 40초에서 레디시 캐시 등록 후 4초로 개선되는 센세이션을 보았다.

 

 

 

Redis를 사용해야 하는 이유

 

Redis 클러스터를 통해 메모리만 충분하다면 그리고 자주 갱신되지 않는 데이터라면 엄청난 속도 향상을 가져다 줌

캐시 사용법

package com.example.config; 
import java.time.Duration; 
import java.util.HashMap; 
import java.util.Map; 
import org.springframework.cache.annotation.EnableCaching; 
import org.springframework.context.annotation.Bean; 
import org.springframework.context.annotation.Configuration; 
import org.springframework.context.annotation.Primary; 
import org.springframework.core.io.ResourceLoader; 
import org.springframework.data.redis.cache.CacheKeyPrefix; 
import org.springframework.data.redis.cache.RedisCacheConfiguration; 
import org.springframework.data.redis.cache.RedisCacheManager; 
import org.springframework.data.redis.cache.RedisCacheManager.RedisCacheManagerBuilder; 
import org.springframework.data.redis.connection.RedisConnectionFactory; 
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; 
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair; 
import org.springframework.data.redis.serializer.StringRedisSerializer; 
/* 
* 기본으로 redis에 cacheName::key 형태로 구분되어 저장됨. key를 생략할 경우, "SimpleKey []" 로 저장됨. 
1. @Cacheable: 읽을때. 
	-예시: @Cacheput 참고. 
2. @CachePut: 갱신 
	-예시: @Cacheable(value="photo", key="#file.fileID", condition="#file.fileName='test'", unless="#result == null", cacheManager="gsonCacheManager") 
3. @CacheEvict: 삭제(cacheManager 지정하면 안됨!) 
	-예시: @CacheEvict(value="photo", key="#file.fileID") 
4. @Caching: 한 메소드에 여러 어노테이션이 필요할때 그룹화 해줌. 
	-예시: @Caching( evict= { @CacheEvict(...), @CacheEvict(...) }, ... ) 
5. 어노테이션 외에 직접 캐시매니저를 통해 캐시 접근이 필요한 경우 
	-서비스 class에서 @Autowired private CacheManager cacheManager; 선언 
    -함수 안에서 cacheManager.getCache("cacheName").evict("key") 처럼 처리하면 됨. 
*/ 

@Configuration 
@EnableCaching public class RedisCacheConfig { 

  //************************ 
  // 일반 객체용 캐시매니저 
  //************************ 
  @Primary
  @Bean(name = "cacheManager") 
  public RedisCacheManager cacheManager(RedisConnectionFactory cf, ResourceLoader rl) { // 기본 캐시매니저(객체 통째로 보관) 

      RedisCacheManagerBuilder builder= RedisCacheManagerBuilder.fromConnectionFactory(cf); 
      RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig(rl.getClassLoader()) 
          .disableCachingNullValues() // 널값 금지(캐싱시 unless("#result == null") 필수.) 
          .entryTtl(Duration.ofDays(1)) // 기본 캐시 1일 유지. 
          .computePrefixWith(CacheKeyPrefix.simple()) // name::key 처럼 key앞에 '::'를 삽입(redis-cli에서 get "name::key" 로 조회.) 
          .serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer())); 

      // 캐시별로 유효시간 다르게 정하기. 
      Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>(); 

      // 3초 
      Duration d3s= Duration.ofSeconds(3); 
      cacheConfigurations.put("pageHot100Talk", configuration.entryTtl(d3s)); 

      // 
      10분 Duration d10m= Duration.ofMinutes(10); 
      cacheConfigurations.put("listKeyword", configuration.entryTtl(d10m)); 

      // 1시간 
      Duration d1h= Duration.ofHours(1); 
      cacheConfigurations.put("listExMain", configuration.entryTtl(d1h)); 

      return builder.cacheDefaults(configuration).withInitialCacheConfigurations(cacheConfigurations).build(); 
  } 

  //************************ 
  // generic json용 캐시매니저 
  //************************ 

  @Bean(name = "gsonCacheManager") 
  public RedisCacheManager gsonCacheManager(RedisConnectionFactory cf, ResourceLoader rl) { // json으로 값 보관(detache가 필요한 entity들- 회원정보, ... 등) 

      RedisCacheManagerBuilder builder= RedisCacheManagerBuilder.fromConnectionFactory(cf); 
      RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig(rl.getClassLoader()) 
          .disableCachingNullValues() 
          .entryTtl(Duration.ofDays(1)) // 기본 캐시 1일 유지. 
          .computePrefixWith(CacheKeyPrefix.simple()) 
          .serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer())) 
          .serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); // json형식으로 value 저장. 

       // 캐시별로 유효시간 다르게 정하기.
       Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>(); 

       // 1시간 
       Duration d1h= Duration.ofHours(1); 
       cacheConfigurations.put("listCategory", configuration.entryTtl(d1h)); 
       return builder.cacheDefaults(configuration).withInitialCacheConfigurations(cacheConfigurations).build(); 
  } 
}

 

Redis 사용

@Component
@RequiredArgsConstructor
public class RedisUtils {
    private final RedisTemplate<String, Object> redisTemplate;
    private final ModelMapper modelMapper;

    public void put(String key, Object value, Long expirationTime){
        if(expirationTime != null){
            redisTemplate.opsForValue().set(key, value, expirationTime, TimeUnit.SECONDS);
        }else{
            redisTemplate.opsForValue().set(key, value);
        }
    }

    public void delete(String key){
        redisTemplate.delete(key);
    }

    public <T> T get(String key, Class<T> clazz){
        Object o = redisTemplate.opsForValue().get(key);
        if(o != null) {
            if(o instanceof LinkedHashMap){
                return modelMapper.map(o, clazz);
            }else{
                return clazz.cast(o);
            }
        }
        return null;
    }

    public boolean isExists(String key){
        return redisTemplate.hasKey(key);
    }

    public void setExpireTime(String key, long expirationTime){
        redisTemplate.expire(key, expirationTime, TimeUnit.SECONDS);
    }

    public long getExpireTime(String key){
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }
}
  • redisTemplate.opsForValue().get(key) : key 값으로 value 를 가져온다
  • redisTemplate.opsForValue().set(key, value, time, unit) : key값으로 value를 저장한다 만약 만료 시간을 지정하는 경우엔 time 과 단위를 함께 세팅한다
  • redisTemplate.hasKey(key) : 키의 존재유무를 return 한다
  • redisTemplate.delete(key) : key값에 대한 데이터를 삭제한다.

RedisTemplate, StringRedisTemplate

Redis는 여러 자료 구조를 가지고 있습니다. 이런 여러 종류의 자료구조를 대응하기 위해 Spring Data Redis는 opsFor[X](ex. opsForValue, opsForSet, opsForZSet 등)라는 메서드를 제공합니다. 해당 메서드를 사용하면 각 자료구조에 대해서 쉽게 Serialize 및 Deserialize 할 수 있습니다.

 

각 메서드에 대한 설명은 아래와 같습니다. (빨간색은 실무에서 많이 사용)

메서드 설명
opsForValue Strings를 쉽게 Serialize / Deserialize 해주는 Interface
opsForList List를 쉽게 Serialize / Deserialize 해주는 Interface
opsForSet Set를 쉽게 Serialize / Deserialize 해주는 Interface
opsForZSet ZSet를 쉽게 Serialize / Deserialize 해주는 Interface
opsForHash Hash를 쉽게 Serialize / Deserialize 해주는 Interface

Strings

Redis의 Strings 자료구조는 opsForValue 메서드를 사용합니다. 사용한 테스트 코드와 결과입니다.

@Autowired
StringRedisTemplate redisTemplate;

@Test
public void testStrings() {
    final String key = "sabarada";

    final ValueOperations<String, String> stringStringValueOperations = redisTemplate.opsForValue();

    stringStringValueOperations.set(key, "1"); // redis set 명령어
    final String result_1 = stringStringValueOperations.get(key); // redis get 명령어

    System.out.println("result_1 = " + result_1);

    stringStringValueOperations.increment(key); // redis incr 명령어
    final String result_2 = stringStringValueOperations.get(key);

    System.out.println("result_2 = " + result_2);
}
result_1 = 1
result_2 = 2

List

Redis의 List 자료구조는 opsForList 메서드를 통해서 쉽게 컨트롤할 수 있습니다.

@Autowired
StringRedisTemplate redisTemplate;

@Test
public void testList() {
    final String key = "sabarada";

    final ListOperations<String, String> stringStringListOperations = redisTemplate.opsForList();

    stringStringListOperations.rightPush(key, "H");
    stringStringListOperations.rightPush(key, "e");
    stringStringListOperations.rightPush(key, "l");
    stringStringListOperations.rightPush(key, "l");
    stringStringListOperations.rightPush(key, "o");

    stringStringListOperations.rightPushAll(key, " ", "s", "a", "b", "a");

    final String character_1 = stringStringListOperations.index(key, 1);

    System.out.println("character_1 = " + character_1);

    final Long size = stringStringListOperations.size(key);

    System.out.println("size = " + size);

    final List<String> ResultRange = stringStringListOperations.range(key, 0, 9);

    System.out.println("ResultRange = " + Arrays.toString(ResultRange.toArray()));
}
character_1 = e
size = 10
ResultRange = [H, e, l, l, o,  , s, a, b, a]

Set

@Test
public void testSet() {
    String key = "sabarada";
    SetOperations<String, String> stringStringSetOperations = redisTemplate.opsForSet();

    stringStringSetOperations.add(key, "H");
    stringStringSetOperations.add(key, "e");
    stringStringSetOperations.add(key, "l");
    stringStringSetOperations.add(key, "l");
    stringStringSetOperations.add(key, "o");

    Set<String> sabarada = stringStringSetOperations.members(key);

    System.out.println("members = " + Arrays.toString(sabarada.toArray()));

    Long size = stringStringSetOperations.size(key);

    System.out.println("size = " + size);

    Cursor<String> cursor = stringStringSetOperations.scan(key, ScanOptions.scanOptions().match("*").count(3).build());

    while(cursor.hasNext()) {
        System.out.println("cursor = " + cursor.next());
    }
}
members = [l, e, o, H]
size = 4
cursor = l
cursor = e
cursor = o
cursor = H

Sorted Set

@Test
public void testSortedSet() {
    String key = "sabarada";

    ZSetOperations<String, String> stringStringZSetOperations = redisTemplate.opsForZSet();

    stringStringZSetOperations.add(key, "H", 1);
    stringStringZSetOperations.add(key, "e", 5);
    stringStringZSetOperations.add(key, "l", 10);
    stringStringZSetOperations.add(key, "l", 15);
    stringStringZSetOperations.add(key, "o", 20);

    Set<String> range = stringStringZSetOperations.range(key, 0, 5);

    System.out.println("range = " + Arrays.toString(range.toArray()));

    Long size = stringStringZSetOperations.size(key);

    System.out.println("size = " + size);

    Set<String> scoreRange = stringStringZSetOperations.rangeByScore(key, 0, 13);

    System.out.println("scoreRange = " + Arrays.toString(scoreRange.toArray()));
}
range = [H, e, l, o]
size = 4
scoreRange = [H, e

Hash

@Test
public void testHash() {
    String key = "sabarada";

    HashOperations<String, Object, Object> stringObjectObjectHashOperations = redisTemplate.opsForHash();

    stringObjectObjectHashOperations.put(key, "Hello", "sabarada");
    stringObjectObjectHashOperations.put(key, "Hello2", "sabarada2");
    stringObjectObjectHashOperations.put(key, "Hello3", "sabarada3");

    Object hello = stringObjectObjectHashOperations.get(key, "Hello");

    System.out.println("hello = " + hello);

    Map<Object, Object> entries = stringObjectObjectHashOperations.entries(key);s

    System.out.println("entries = " + entries.get("Hello2"));

    Long size = stringObjectObjectHashOperations.size(key);

    System.out.println("size = " + size);
}
hello = sabarada
entries = sabarada2
size = 3

출처 :

https://cublip.tistory.com/339

https://gogo-jjm.tistory.com/35