본문 바로가기

Spring/Redis

2) Spring Redis Cache적용하기 - 2부 Redis CacheEvict allEntries=true 문제해결

반응형

1부에 간단하게 Redis cache 를 이용하는 방법과 설정 방법에 대해 알아 보았다. 

 

1부에서 보면 cacheable 된 데이터를 cacheEvict 하는 옵션중에  allEntries= true 라고 되어 있는데

실제 운영 Redis에서는 성능상의 문제로 allEntries=true 했을때 실행되는 Redis Command 가 사용하지 못하게 

막혀있기 때문에 대부분의 운영서비스에서는 사용하지 못한다. 그러면 어떻게 해당 cache value에 대한것들을 

다 지울수 있을까 하는 고민이 생기게 된다. 

 

먼저 allEntries=true를 사용하지 못할때 어떤 문제가 생기는지 확인해 보자.

 

1) findAll로 List 데이터를 조회 한다. 

2) 조회한 list중 하나를 삭제한다. 

3)다시 findAll 로 데이터를 조회한다.

 

정상적이라면 

1) 3개 데이터 조회 

2) 1개 데이터 삭제

3) 2개 데이터 조회 

가 되어야 하는데 실제로 allEntires를 사용하지 않는 상황에서는 그렇지 못한것을 아래 결과에서 볼수 있다.

먼저 간단 소스코드이다.

    @Cacheable(value="content")
    public List<ContentDTO> findAll() throws Exception{
        List<ContentDTO> contentDTOs = new ArrayList<ContentDTO>();
        List<Content> contents = repo.findAll();
        for(Content content : contents){
            contentDTOs.add(ContentDTO.createDTO(content));
        }
        return contentDTOs;
    }
    @CacheEvict(value = "content" , key="{#p0.contentId}")
    public void removeContent(ContentDTO dto) throws Exception{
        repo.deleteById(dto.getContentId());
    }

1) 데이터 조회 결과 

2) 첫번째 데이터 삭제

실제로 DB에서는 데이터가 삭제된것을 확인

 

3) 다시 조회

삭제된 데이터 여전히 조회 되는것을 확인.

 

결과는 당연하게도 findAll 이 실행됐을때 실제 Redis에 저장된 데이터 키는 아래 결과와 같이 저장되어 있는데

removeCotent 할때  evcit key 값은 content::<parameter contentId값> 이 지워 졌을것이므로 

 

findAll 했을때 cache된 데이터는 그대로 남아 있게 된다. 그러면  allEntires= ture 를 사용하지 않고 어떻게 처리를 해야 할까?

 

아래 순서로 로직을 구현해 보았다.

 

1) findAll 했을경우 Redis에 list 형태의 cache key 들을 저장

2) remove나 cache evict를 일어났을때  1) 에서 저장했던  cache key 들을 하나씩 가져와서 삭제

단순하게 이런 프로세로 진행을 하게 되는데 아래 소스 완성된 소스코드를 참조.

 

package com.devracoon.redis.service;

import com.devracoon.redis.controller.ContentDTO;
import com.devracoon.redis.entity.Content;
import com.devracoon.redis.repository.ContentRepository;
import com.devracoon.redis.util.RedisListCacheEvict;
import com.devracoon.redis.util.RedisListCacheable;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class ContentServiceImpl {

    private final ContentRepository repo;

    @Transactional
    @RedisListCacheEvict(value="CONTENTLIST")
    public ContentDTO addContent(ContentDTO dto) throws Exception{
        Content content = Content.builder().contentName(dto.getContentName()).contentType(dto.getContentType()).build();
        return ContentDTO.createDTO(repo.save(content));
    }

    @Transactional
    @RedisListCacheEvict(value="CONTENTLIST")
    @CacheEvict(value = "content" , key="{#p0.contentId}")
    public void removeContent(ContentDTO dto) throws Exception{
        repo.deleteById(dto.getContentId());
    }

    @Cacheable(value = "content" , key="{#p0.contentId}")
    public ContentDTO findContent(ContentDTO dto) throws Exception{
        log.info("start findContent .. ");
        Content content = repo.findById(dto.getContentId()).orElse(null);
//        List<ContentDTO> contentDTOs = contents.stream().map(c -> ContentDTO.createDTO(c)).collect(Collectors.toList());
        ContentDTO contentDTO = ContentDTO.createDTO(content);
        return contentDTO;
    }

    @RedisListCacheable(value="CONTENTLIST")
    public List<ContentDTO> findAll() throws Exception{
        List<ContentDTO> contentDTOs = new ArrayList<ContentDTO>();
        List<Content> contents = repo.findAll();
        for(Content content : contents){
            contentDTOs.add(ContentDTO.createDTO(content));
        }
        return contentDTOs;
    }
}
package com.devracoon.redis.util;

import com.google.common.base.Joiner;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

@Component
@RequiredArgsConstructor
@Aspect
@Slf4j
public class RedisListCacheAspect {

    private final RedisCacheUtils redisCacheUtils;

    @Around("@annotation(RedisListCacheable)")
    public Object listCacheable(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        RedisListCacheable redisListCacheable = AnnotationUtils.getAnnotation(methodSignature.getMethod() , RedisListCacheable.class);

        String key = redisListCacheable.value();
        Object [] args = joinPoint.getArgs();
        if(args.length > 0 ){
            key = key + Joiner.on(":").join(joinPoint.getArgs());
        }else{
            key = key + "noArgs[]";
        }

        log.info("key {}   , join point args : {}" , key , joinPoint.getArgs() );
        Object result = redisCacheUtils.get(key);
        if(ObjectUtils.isEmpty(result)){
            result = joinPoint.proceed();
            redisCacheUtils.set(key, result, redisListCacheable.value());
        }
        return result;
    }

    @Around("@annotation(RedisListCacheEvict)")
    public Object listCacheEvict(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        RedisListCacheEvict redisListCacheEvict = AnnotationUtils.getAnnotation(methodSignature.getMethod() , RedisListCacheEvict.class);
        log.info("RedisListCacheEvict value : {}" , redisListCacheEvict.value() );
        redisCacheUtils.evict(redisListCacheEvict.value());
        return joinPoint.proceed();

    }
}
package com.devracoon.redis.util;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;

@Slf4j
@Component
@RequiredArgsConstructor
public class RedisCacheUtils<K, V> {

	private final RedisTemplate redisTemplate;
	private final static long MAX_SET_SIZE = 20;

	public V get(K key) {
		return (V) redisTemplate.opsForValue().get(key.toString());
	}

	public void set(K key, V value, String listName) {
		// 리스트에 저장
		// find all

		List<String> keys = redisTemplate.opsForList().range(listName, 0, -1);
		log.info("keys : {}" , keys);
		if (! keys.contains(key) ) {
			redisTemplate.opsForList().leftPush(listName, key.toString());
			if ( keys.size() > MAX_SET_SIZE) {
				// 가장 오래된 아이템 제거
				Object pop = redisTemplate.opsForList().rightPop(listName);
				redisTemplate.delete(pop);
			}
		}

		redisTemplate.opsForValue().set(key.toString() , value, Duration.ofMinutes(1L));
	}

	public void evict(String setName) {
		List<String> keys =  redisTemplate.opsForList().range(setName, 0, -1);

		log.info("evict keys {}" , keys);
		for (String key : keys) {
			redisTemplate.delete(key);
		}
		log.info("redisTemplate.delete {}" , setName);
		redisTemplate.delete(setName);
	}

}

Git Repo :

https://github.com/devraccon/SpringRedisTest.git

'Spring > Redis' 카테고리의 다른 글

1) Spring Redis Cache적용하기 - 1부 Springboot 설정  (0) 2021.07.16