[Redis] 샤드를 지원하는 SharedJedisTemplate 만들기

Published on: 2013. 12. 9. 18:38 by louis.dev

spring-data-redis에서 제공해주는 RedisTemplate을 사용하면 @Cacheable 어노테이션을 사용할 수도 있고, Jedis를 이용한 Redis 사용을 쉽게 개발할수 있는 장점이 있습니다.


하지만 현재 spring-data-redis 1.1.0 버전에서 제공해 주는 RedisTemplate은 샤드를 지원해 주지 않고 있기 때문에 시스템 구성이 샤드로 되어 있는 경우 spring-data-redis의 RedisTemplate을 사용할 수가 없습니다.


그래서 샤드를 지원하는 레디스 템플릿을 만들어 보기 위해 구글링을 하던 중 중국 개발자가 만들어 놓은 ShardedJedisTemplate를 찾을수 있었고, 약간의 수정을 하여 Template 클래스를 만들었습니다.



1. RedisException

package net.krespo.redis.exception;

public class RedisException extends RuntimeException{
	private static final long serialVersionUID = 9150947297211556907L;

	public RedisException() {
		super();
	}

	public RedisException(String message, Throwable cause) {
		super(message, cause);
	}

	public RedisException(String message) {
		super(message);
	}

	public RedisException(Throwable cause) {
		super(cause);
	}

}

2. ShardedJedisPoolUtils

package net.krespo.redis.jedis.util;


import net.krespo.redis.jedis.ShardedJedisCallback;
import net.krespo.redis.jedis.ShardedJedisReturnedCallback;
import redis.clients.jedis.ShardedJedis;
import redis.clients.jedis.ShardedJedisPool;
import redis.clients.jedis.exceptions.JedisConnectionException;

/**
 * 레디스의 실행 및 실행결과를 리턴해주는 유틸
 * **/
public class ShardedJedisPoolUtils {
	
	/**
	 * 리턴값이 필요한 Command 실행
	 * */
	public static  T execute(ShardedJedisPool jedisPool, ShardedJedisReturnedCallback shardedJedisReturnedCallback) {
		ShardedJedis shardedJedis = null;
	    try {   
	    	shardedJedis = jedisPool.getResource();
	        T returnValue = shardedJedisReturnedCallback.doInShardedJedis(shardedJedis);
	    	return returnValue;
	    } 
	    //장애로 인해 레디스 커넥션이 맺어지지 않으면 커넥션풀에서 해당 커넥션을 제거한다.
	    catch (JedisConnectionException e) {
	    	if(shardedJedis != null) jedisPool.returnBrokenResource(shardedJedis);
	    	return null;
	    }
	    finally{
	    	if (shardedJedis != null) {
	    		jedisPool.returnResource(shardedJedis);
	    	}
	    }
	}
	
	/**
	 * 리턴값이 필요 없는 Command 실행
	 * */
	public static void execute(ShardedJedisPool jedisPool, ShardedJedisCallback shardedJedisCallback) {
		ShardedJedis shardedJedis = null;
	    try {   
	    	shardedJedis = jedisPool.getResource();
	        shardedJedisCallback.doInShardedJedis(shardedJedis);
	    } 
	    //장애로 인해 레디스 커넥션이 맺어지지 않으면 커넥션풀에서 해당 커넥션을 제거한다.
	    catch (JedisConnectionException e) {
	    	if(shardedJedis != null) jedisPool.returnBrokenResource(shardedJedis);
	    }
	    finally{
	    	if (shardedJedis != null) {
	    		jedisPool.returnResource(shardedJedis);
	    	}
	    }
	}
}

3. ShardedJedisCallback

package net.krespo.redis.jedis;

import redis.clients.jedis.ShardedJedis;
public interface ShardedJedisCallback {
	void doInShardedJedis(ShardedJedis shardedJedis);
}

4. ShardedJedisReturnedCallback

package net.krespo.redis.jedis;

import redis.clients.jedis.ShardedJedis;

public interface ShardedJedisReturnedCallback {
	T doInShardedJedis(ShardedJedis shardedJedis);
}

5. AbstractRedisTemplate

package net.krespo.redis.template;

import net.krespo.redis.exception.RedisException;

public abstract class AbstractRedisTemplate {
	protected void handleException(RedisException e) throws RedisException {
		throw e;
	}
	
	protected RedisException wrapperException(Exception e) {
		return new RedisException(e.getMessage(), e);
	}
}

6. ShardedJedisTemplate

package net.krespo.redis.jedis;

import net.krespo.redis.exception.RedisException;
import net.krespo.redis.jedis.util.ShardedJedisPoolUtils;
import net.krespo.redis.template.AbstractRedisTemplate;
import redis.clients.jedis.ShardedJedisPool;

public class ShardedJedisTemplate extends AbstractRedisTemplate{
	protected ShardedJedisPool shardedJedisPool;
	
	public void setShardedJedisPool(ShardedJedisPool shardedJedisPool) {
		this.shardedJedisPool = shardedJedisPool;
	}
	
	
	public  T execute(ShardedJedisReturnedCallback shardedJedisReturnedCallback) throws RedisException {
		try {
			return ShardedJedisPoolUtils.execute(shardedJedisPool, shardedJedisReturnedCallback);
		} catch (Exception e) {
			handleException(wrapperException(e));
			return null;		
		}
	}
	
	
	public void execute(ShardedJedisCallback shardedJedisCallback) throws RedisException {
		try {
			ShardedJedisPoolUtils.execute(shardedJedisPool, shardedJedisCallback);
		} catch (Exception e) {
			handleException(wrapperException(e));
		}
	}
}

사용법은 아래와 같이 사용하면 됩니다.

package net.krespo.redis.template.jedis;

import java.util.ArrayList;
import java.util.List;

import net.krespo.redis.jedis.ShardedJedisCallback;
import net.krespo.redis.jedis.ShardedJedisReturnedCallback;
import net.krespo.redis.jedis.ShardedJedisTemplate;

import org.junit.Assert;
import org.junit.Before;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;

import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisShardInfo;
import redis.clients.jedis.ShardedJedis;
import redis.clients.jedis.ShardedJedisPool;

public class ShardedJedisTemplateTests {
	private JedisPoolConfig config;
	private List jedisShardInfos;
	private ShardedJedisPool pool;
	private ShardedJedisTemplate template;
	@Before
	public void init() {
		config = new JedisPoolConfig();
		jedisShardInfos = new ArrayList();
		
		JedisShardInfo shard1 = new JedisShardInfo("호스트1", 포트);
		JedisShardInfo shard2 = new JedisShardInfo("호스트2", 포트);
		shard1.setPassword("비밀번호");
		shard2.setPassword("비밀번호");
		
		jedisShardInfos.add(shard1);
		jedisShardInfos.add(shard2);
		
		pool = new ShardedJedisPool(config, jedisShardInfos);
		
		template = new ShardedJedisTemplate();
		template.setShardedJedisPool(pool);
	}
	
	@Test
	public void shardedJedisTemplateCallbackTest() {
		template.execute(new ShardedJedisCallback() {
			public void doInShardedJedis(ShardedJedis shardedJedis) {
				String status = shardedJedis.set("test:key", "testvalue");
				Assert.assertEquals("OK", status);
			}
		});
	}
	@Test
	public void shardedJedisTemplateReturnedCallbackTest() {
		String value = template.execute(new ShardedJedisReturnedCallback() {
					   		public String doInShardedJedis(ShardedJedis shardedJedis) {
								return shardedJedis.get("test:key");
							}
							
						});
		Assert.assertNotNull(value);
		Assert.assertEquals("testvalue", value);
		
	
	}
    
}

[Redis] Redis GUI Tool - Redis Desktop Manager를 소개합니다

Published on: 2013. 11. 27. 17:17 by louis.dev

Oracle의 SQL Developer, MySQL의 MySQL WorkBench같이 Redis도 Redis Desktop Manager라는 툴이 존재합니다. 

Redis Desktop Manager는 Windows 뿐만 아니라 Linux, Mac OS X까지 지원해주고 있습니다.



설치하는 방법은 간단합니다.


다운로드 받은 exe파일을 더블클릭하면 간단하게 설치가 완료됩니다. 

설치가 완료된 후 Redis Desktop Manager를 실행해 보면 이런 화면이 뜨게 됩니다.



이 화면에서 왼쪽 하단에 있는 Add New Connection을 선택하여 Redis 서버의 정보를 입력합니다.



위의 화면에 레디스 서버 정보를 모두 입력하면 간단하게 서버가 추가가 됩니다.



추가된 서버를 더블클릭하게 되면 서버와 커넥션이 맺어지고 간단하게 Redis의 DB와 해당 DB에 들어가있는 데이터를 한눈에 파악할 수 있습니다.



뿐만 아니라 레디스 서버 정보에서 오른쪽 마우스클 클릭하면 Console 이란 메뉴가 나오는데 이메뉴를 선택하면 레디스 커맨드를 직업 입력할 수가 있습니다.



위에서 쓴 대로 기능 자체는 매우 심플합니다만 레디스 서버를 사용하기에는 크게 부족하지는 않는것 같습니다

[Redis-Jedis] Pipeline을 이용한 다량의 데이터 Insert하기

Published on: 2013. 11. 19. 21:10 by louis.dev

대량의 데이터를 레디스에 저장해야 할때는 Pipeline을 이용하면 단순히 set command보다 월등한 성능을 보장받을 수 있습니다.


사용은 간단합니다.

@Autowired protected ShardedJedisPool redisPool;
public void setByPipeline(Map<string, string> map) {
	ShardedJedis jedis = null;
	try {
		jedis = redisPool.getResource();
		ShardedJedisPipeline pipeline =  jedis.pipelined();
		
		for(String key : map.keySet()) {
			
			pipeline.set(key, map.get(key));
		}
		pipeline.sync();
	} catch (JedisConnectionException e) {
		if (jedis != null) redisPool.returnBrokenResource(jedis); // 접속이 끊긴 리소스 반환
	} finally {
		if (jedis != null) redisPool.returnResource(jedis);
	}
}

pipeline에 삽입하고자 하는 데이터를 set을 먼저해줍니다. 이때 set커맨드는 실제로 데이터를 서버로 전송하지 않고 12번째 줄인 pipeline.sync()를 콜한후에 실제 데이터를 서버로 전송하여 저장하게 됩니다.


파이프라인 방식은 명령어 자체를 파일로 쓴후 해당 파일을 레디스 서버로 전송하여 대용량 삽입을 빠르게 할수 있게 합니다.


테스트 결과 단순 데이터 5000건을 set 해봤더니 약 200ms 정도 걸리네요~

[Redis-Jedis] Spring을 이용한 샤드(Shard) 설정

Published on: 2013. 11. 19. 20:55 by louis.dev

업무에 Redis를 이용할 기회가 있어서 설정 내용을 포스팅해봅니다. Redis의 Java 라이브러리인 Jedis와 Spring FrameWork를 통해서 세팅해보겠습니다.


Spring에서는 spring-data라는 프로젝트를 통해 각종 Database, NoSQL의 사용을 스프링프레임워크를 기반으로한 프로젝트에서 쉽게 사용할수있도록 라이브러리를 제공하고 있습니다.


물론 Redis도 spring-data-redis라는 이름으로 제공하고 있습니다. 그런데 spring-data-redis 프로젝트가 시작된지 얼마 안되서 그런지(현재 안정화 버전은 1.1.0) 샤딩을 지원하고 있지 않습니다.


여기서 샤딩이란 간단하게 말하자면 여러대의 레디스 서버에 데이터를 분산하여 저장하는 것을 의미합니다. 

Redis는 데이터를 메모리에 올려놓는 In Memory 방식이기 때문에 SSD나 HDD에 저장하는 기존의 DB들 보다 저장공간이 부족합니다(하드디스크는 테라단위로 설치하긴 쉽지만 램은 하드디스크 보다 용량 확장이 쉽지 않기 때문입니다).

따라서 레디스 서버 한대로 서비스를 시작한 후에 데이터량이 점점 증가하여 메모리 용량이 모자르게 되면 서버를 추가하여 데이터를 해당 서버 수만큼 쪼개어 저장할수 있는데 이것을 샤딩이라고 합니다.


아무튼 spring-data-redis는 Sharding을 지원해 주지 않기 때문에 Jedis를 이용하여 직접 샤드 설정을 해야합니다.


샤드 설정은 생각보다 간단합니다.


1. Jedis를 다운받은 후 프로젝트에 라이브러리를 추가합니다.

2. xml에 다음과 같이 bean을 등록합니다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:c="http://www.springframework.org/schema/c"
	xmlns:p="http://www.springframework.org/schema/p"
	xmlns:util="http://www.springframework.org/schema/util"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
    
	<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig" p:maxActive="500" p:maxIdle="300" p:maxWait="10000" p:testOnBorrow="true" />
	<util:list id="jedisShardInfos">
		<bean class="redis.clients.jedis.JedisShardInfo" c:host="${redis1.host.name}" c:port="${redis.port}" c:timeout="10000" c:weight="1" />
		<bean class="redis.clients.jedis.JedisShardInfo" c:host="${redis2.host.name}" c:port="${redis.port}" c:timeout="10000" c:weight="1" />
	</util:list>
	<!-- 해쉬 알고리즘 MURMUR와 MD5가 있음 -->
	<util:constant id="jedisAlgo" static-field="redis.clients.util.Hashing.MURMUR_HASH" />
	<util:constant id="jedisKeyTagPattern" static-field="redis.clients.util.Sharded.DEFAULT_KEY_TAG_PATTERN" />
	<bean id="shardedJedisPool" class="redis.clients.jedis.ShardedJedisPool" c:poolConfig-ref="jedisPoolConfig" c:shards-ref="jedisShardInfos" c:algo-ref="jedisAlgo" c:keyTagPattern-ref="jedisKeyTagPattern" destroy-method="destroy" />	
</beans>

3. Jedis를 이용해 실제 Redis Command를 수행할 Template Class를 생성합니다.

package net.krespo.template;

import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import redis.clients.jedis.ShardedJedis;
import redis.clients.jedis.ShardedJedisPipeline;
import redis.clients.jedis.ShardedJedisPool;
import redis.clients.jedis.exceptions.JedisConnectionException;

@Component
public class RedisCacheTemplate {
	
	@Autowired protected ShardedJedisPool redisPool;
	public void set(String key, String value) {
		ShardedJedis jedis = null;
		try {
		    jedis = redisPool.getResource();
		    jedis.set(key,value)
		} catch (JedisConnectionException e) {
			if (jedis != null) redisPool.returnBrokenResource(jedis); // 접속이 끊긴 리소스 반환
		} finally {
		    if (jedis != null) redisPool.returnResource(jedis);
		}
	}
}

위의 코드에서 눈여겨 볼부분은 24번째 줄입니다. 

JedisConnectionException이 발생하면 redisPool의 returnBrokenResource를 호출하고 있는데요. 

샤딩을 구성하는 여러대의 서버중 한대가 장애가 발생하면, 커넥션 풀에는 장애나기 전 서버와의 커넥션이 그대로 존재하게 됩니다. 따라서 어플리케이션에서 커넥션풀에서 커넥션을 꺼내올것을 요청했을때(redisPool.getResource) 장애가 발생한 서버의 커넥션을 리턴해주고, 해당커넥션으로 레디스 커맨드를 실행하려고 하면 JedisConnectionException이 발생하게 됩니다. 따라서 해당 익셉션 발생시 풀에서 해당 커넥션을 제거해주는 작업을 해주기 위함입니다.


RedisCacheTemplate 클래스는 간단하게 jedis를 이용한 set메소드를 구현하였습니다. 위와 같이 Template 클래스를 만든 이유는 레디스 커맨드를 사용하기 전에 항상

  1. 커넥션 풀에서 커넥션 획득
  2. 커맨드 실행후 해당 커넥션 반환

의 작업을 항상 해주어야 하는데요. 이 번거러운 작업을 미리 작업해 두고 다른 persistance 레이어의 클래스에서는 해당 Template 클래스를 주입시켜 사용만 하면 되기 때문입니다.

ShardedJedis jedis = null;
try {
	jedis = redisPool.getResource();
	//레디스 커맨드 실행
} catch (JedisConnectionException e) {
	if (jedis != null) redisPool.returnBrokenResource(jedis); // 접속이 끊긴 리소스 반환
} finally {
	if (jedis != null) redisPool.returnResource(jedis);
}

위의 예제에서는 set메소드만 구현하였지만 상황에 따라 필요한 메소드를 구현하면 됩니다. 

그리고 redisPool.getResource와 redisPool.returnResource 같은 부분이 계속나와 신경쓰이신다면 AOP로 빼서 한꺼번에 적용하셔도 좋을것 같네요~


이상 스프링기반의 프로젝트에서 Jedis를 이용한 샤딩 설정을 마치겠습니다