2013년 3월 26일 화요일

Spring 3.1 + Redis 를 이용한 Cache

이번 주에는 Spring 3.1 에서 지원하는 Cache 관련해서 많은 글을 썼는데, 요즘 가장 많이 사용되는 Redis 를 저장소로 사용하는 Cache 를 만들겠습니다.

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);
}
}
}
view raw RedisCache.java hosted with ❤ by GitHub


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 구성 시에는 가장 먼저 고려되어야 할 캐시 저장소라 생각됩니다.

저는 앞으로 hibernate  2nd cache provider for redis,  hibernate-ogm-redis 를 만들어 볼 예정입니다.

댓글 없음: