IT_Programming/Dev Libs & Framework

[Spring] Spring 3.1 + Redis 를 이용한 Cache

JJun ™ 2015. 7. 9. 00:46



 출처: http://debop.blogspot.kr/2013/03/spring-31-redis-cache.html





Redis 를 구현하기 위해서 spring-data-redis 와 jedis 라이브러리를 사용했습니다.
jedis 만으로도 구현할 수 있지만, 편하게 spring-data-redis 의 RedisTemplate 를 사용하기로 했습니다.

우선 Redis 를 캐시 저장소로 사용하기 위해 환경설정을 합니다.

1. RedisCacheConfiguration.java


@Configuration
@EnableCaching
@ComponentScan(basePackageClasses = UserRepository.class)
// @PropertySource("classpath:redis.properties")
public class RedisCacheConfiguration {
    @Autowired
    Environment env;
    @Bean
    public JedisShardInfo jedisShardInfo() {
        return new JedisShardInfo("localhost");
    }
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new JedisConnectionFactory(jedisShardInfo());
    }
    @Bean
    public RedisTemplate redisTemplate() {
        RedisTemplate<String,Object> template = new RedisTemplate<String,Object>();
        template.setConnectionFactory(redisConnectionFactory());
        return template;
    }
    @Bean
    public RedisCacheManager redisCacheManager() {
        return new RedisCacheManager(redisTemplate(), 300);
    } 

}


한가지 RedisCacheFactory를 생성할 때 주의할 점은 JedisShardInfo 로 생성해야지,
JedisPoolingConfig나 기본 생성자로 생성 시에 RedisTemplate 에서 connection을 제대로 생성 못하는 버그가 있더군요 ㅠ.ㅠ
이것 때문에 반나절을 허비...




2. RedisCacheManager.java


@Slf4j

public class RedisCacheManager extends AbstractTransactionSupportingCacheManager {
    private RedisTemplate redisTemplate;
    private int expireSeconds;
    public RedisCacheManager(RedisTemplate redisTemplate) {
        this(redisTemplate, 300);
    }
    public RedisCacheManager(RedisTemplate redisTemplate, int expireSeconds) {
        Guard.shouldNotBeNull(redisTemplate, "redisTemplate");
        this.redisTemplate = redisTemplate;
        this.expireSeconds = expireSeconds;
    }
    @Override
    protected Collection<? extends Cache> loadCaches() {
        Collection<Cache> caches = Lists.newArrayList();
        for (String name : getCacheNames()) {
            caches.add(new RedisCache(name, redisTemplate, expireSeconds));
        }
        return caches;
    }
    @Override
    public Cache getCache(String name) {
        synchronized (this) {
            Cache cache = super.getCache(name);
            if (cache == null) {
                cache = new RedisCache(name, redisTemplate, expireSeconds);
                addCache(cache);
            }
            return cache;
        }
    }
}





 3. RedisCache.java


@Slf4j

public class RedisCache implements Cache {
    @Getter
    private String name;
    @Getter
    private int expireSeconds;
    private RedisTemplate redisTemplate;
    public RedisCache(String name, RedisTemplate redisTemplate) {
        this(name, redisTemplate, 300);
    }
    public RedisCache(String name, RedisTemplate redisTemplate, int expireSeconds) {
        Guard.shouldNotBeEmpty(name, "name");
        Guard.shouldNotBeNull(redisTemplate, "redisTemplate");
        this.name = name;
        this.redisTemplate = redisTemplate;
        if (log.isDebugEnabled())
            log.debug("MongoCache를 생성합니다. name=[{}], mongodb=[{}]", name, redisTemplate);
    }
    @Override
    public Object getNativeCache() {
        return redisTemplate;
    }
    public String getKey(Object key) {
        return name + ":" + key;
    }
    @Override
    public ValueWrapper get(Object key) {
        Guard.shouldNotBeNull(key, "key");
        if (log.isDebugEnabled())
            log.debug("캐시 키[{}] 값을 구합니다...", key);
        Object result = redisTemplate.opsForValue().get(getKey(key));
        SimpleValueWrapper wrapper = null;
        if (result != null) {
            if (log.isDebugEnabled())
                log.debug("캐시 값을 로드했습니다. key=[{}]", key);
            wrapper = new SimpleValueWrapper(result);
        }
        return wrapper;
    }
    @Override
    @SuppressWarnings("unchecked")
    public void put(Object key, Object value) {
        Guard.shouldNotBeNull(key, "key");
        if (log.isDebugEnabled())
            log.debug("캐시에 값을 저장합니다. key=[{}], value=[{}]", key, value);
        redisTemplate.opsForValue().set(getKey(key), value, expireSeconds);
    }
    @Override
    @SuppressWarnings("unchecked")
    public void evict(Object key) {
        Guard.shouldNotBeNull(key, "key");
        if (log.isDebugEnabled())
            log.debug("지정한 키[{}]의 캐시를 삭제합니다...", key);
        try {
            redisTemplate.delete(key);
        } catch (Exception e) {
            log.error("캐시 항목 삭제에 실패했습니다. key=" + key, e);
        }
    }
    @Override
    @SuppressWarnings("unchecked")
    public void clear() {
        if (log.isDebugEnabled())
            log.debug("모든 캐시를 삭제합니다...");
        try {
            redisTemplate.execute(new RedisCallback() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    connection.flushAll();
                    return null;
                }
            });
        } catch (Exception e) {
            log.warn("모든 캐시를 삭제하는데 실패했습니다.", e);
        }
    }
}



RedisCache의 get / put 은 일반적으로 쓰는 opsForValue() 를 사용했습니다.
다른 것을 사용할 수도 있을텐데, 좀 더 공부한 다음에 다른 것으로 변경해 봐야 할 듯 합니다.

마지막에 clear() 메소드도 jedis 에는 flushDB(), flushAll() 메소드를 지원하는데,
RedisTemplate에서는 해당 메소드를 expose 하지 않아 코드와 같이 RedisCallback 을 구현했습니다.




4. RedisCacheTest.java

@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {RedisCacheConfiguration.class})
public class RedisCacheTest {
    @Autowired
    RedisCacheManager redisCacheManager;
    @Autowired
    UserRepository userRepository;
    @Test
    public void clearTest() {
        Assert.assertNotNull(redisCacheManager);
        Cache cache = redisCacheManager.getCache("user");
        Assert.assertNotNull(cache);
        cache.clear();
        Assert.assertNotNull(cache);
    }
    @Test
    public void getUserFromCache() {
        Stopwatch sw = new Stopwatch("initial User");
        sw.start();
        User user1 = userRepository.getUser("debop", 100);
        sw.stop();
        sw = new Stopwatch("from Cache");
        sw.start();
        User user2 = userRepository.getUser("debop", 100);
        sw.stop();
        Assert.assertEquals(user1.getUsername(), user2.getUsername());
    }
    @Test
    public void componentConfigurationTest() {
        Assert.assertNotNull(redisCacheManager);
        Cache cache = redisCacheManager.getCache("user");
        Assert.assertNotNull(cache);
        cache.evict("debop");
        Assert.assertNotNull(userRepository);
    } 

}



UserRepository는 전에 쓴 Spring 3.1 + EhCache 등의 글과 같은 코드라 생략했습니다.
Redis 관련은 Windows 에서는 구 버전만 지원하고, 신 버전은 linux 만 가능하더군요...
그래도 성능은 정평이 나있으니, HA 구성 시에는 가장 먼저 고려되어야 할 캐시 저장소라 생각됩니다.