JAVA

5분 안에 구축하는 Redis(레디스)

코린이s 2021. 2. 15. 17:40
728x90

레디스(Remote Dictionary Server)가 무엇인가!

- 메모리 기반 저장 서비스

- 키-값 구조

- 문자열만 저장할 수 있는 Memcached와 달리 다양한 자료구조(Collection: 목록성 데이터 처리 구조) 지원 -> 다양한 형식으로 저장 가능 (String, Set, Sorted Set, Hash, List 등)

- 오픈 소스 (무료)

- 원격(Remote) 저장소로 여러 서버에서 같은 데이터를 공유할 수 있음

- 캐시로 사용 가능

- 메모리에만 저장하는 Memcached와 달리 디스크에도 저장하여 데이터 저장소로 사용 가능 (백업 방식으로 2가지가 존재함.)

> 개념만 봤을때 아 ~ 그렇구나 정도지 실제 해보지 않으면 알기 어렵다.. (다음번에 테스트 해볼 예정)

  RDB AOF (Append Only File)
저장 방법 메모리에 있는 데이터 전체를 바이너리 파일로 저장하는 방식 명령이 실행될 때마다 해당 명령이 파일에 기록되는 방식
저장 시점 - 설정 : save [time], [count]
- 지정 하는 일정 time 동안 count 만큼의 key 변경이 발생하면 rdb 파일로 저장됨
- 설정 : appendfsync [always | everysec | no]
- 명령어 실행시 / 1초 마다 / 기록하는 시점을 OS 가 지정
로드 시간 RDB파일이 사이즈가 작아 AOF파일 보다 빨리 로드 가능
저장(백업) 파일 dump.rdb appendonly.aof
기능   # rewrite 기능
- 특정 시점에 데이터 전체를 다시 쓰는 기능
- 같은 key를 100번 저장하는 경우 100번이 모두 기록 되지만 rewrite 실행시 최종 수정된 마지막 값만 남아 파일 사이즈가 작아짐
읽기 가능 바이너리 파일이라 사람이 읽을 수 없음 텍스트 파일이라 파일을 읽을 수 있음 (잘못 입력한 명령어는 수정 하여 명령어 취소가 가능)
참고 - 스냅샷을 일정 간격으로 HDD에 저장하게 가능하며, 저장 간격에 따라 데이터 손실 정도가 달라짐

- 데이터 세트가 매우 크고 CPU 성능이 좋지 않은 경우 Redis가 몇 밀리 초 동안 또는 심지어 1 초 동안 클라이언트 서비스를 중지 할 수 있음
 

참고 :: redis 장점도 많지만 캐시에 저장된 데이터가 사라져도 괜찮다면 Memcached 는 멀티 스레드(실행 흐름 단위)로 처리하여 성능이 좋기(빠른 처리) 때문에 Memcached 사용하는 편이 좋을 것 같다.

설치를 해봅시다! (mac 기준)

$brew install redis

설치가 완료되면 아래 서비스 시작 명령어로 서비스를 시작 해줍니다.

- homebrew로 서비스 시작
$brew services start redis

- 서비스 중단 
$brew services stop redis

- 서비스 재기동
$brew services restart redis

- redis 클라이언트로 접속
$redis-cli

 

프로젝트를 만들어 봅시다!

#  개발환경

- mac os catalina
- java 15
- spring boot 2.4.2
- gradle 6.6.1- mac os catalina

:: github.com/works-code/redis

 

works-code/redis

redis. Contribute to works-code/redis development by creating an account on GitHub.

github.com

# 레디스 port, ip 정의

package com.code.redis.configuration;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

// 해당 설정을 해도 되고, application.yml에 설정 해도 됨
@ConfigurationProperties(prefix = "spring.redis")
@Component
public class RedisProperties {
    private String host;
    private int port;
}
// env.properties
spring.redis.host=localhost
spring.redis.port=6379

# 설정 (각자 사용하고자 하는 라이브러리에 맞게 쓰시면 됩니다.)

package com.code.redis.configuration;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import redis.clients.jedis.JedisPoolConfig;

@EnableRedisRepositories
@Configuration
public class RedisConfiguration {

    @Autowired
    RedisProperties redisProperties;

	// jedis 사용시 해당 빈을 사용
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new JedisConnectionFactory(new RedisStandaloneConfiguration(redisProperties.getHost(), redisProperties.getPort()));
    }

	// lettuce 사용시 해당 빈을 사용
    /*@Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(new RedisStandaloneConfiguration(redisProperties.getHost(), redisProperties.getPort()));
    }*/
}

# 라이브러리 정의 (각자 사용하고자 하는 라이브러리에 맞게 쓰시면 됩니다.)

dependencies {
    ...
    // jedis 사용시 해당 부분 정의
    compile ('org.springframework.boot:spring-boot-starter-data-redis:2.3.1.RELEASE') {
        exclude(group: 'io.lettuce', module: 'lettuce-core')
    }
    compile group: 'redis.clients', name: 'jedis'

    // lettuce 사용시 해당 부분 정의
    /*compile ('org.springframework.boot:spring-boot-starter-data-redis:2.3.1.RELEASE')*/
}

 

# 레디스 서비스(사용을 간편하게 하기 위해 서비스 구성)

package com.code.redis.service;

import com.code.redis.vo.Struct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.NumberUtils;
import org.springframework.util.ObjectUtils;

import java.util.*;
import java.util.concurrent.TimeUnit;

/***
 * String에 특화된 StringRedisTemplate
 * String 만 다루려면 아래 서비스 사용하고, 그게 아니라면 RedisTemplate 빈 정의하여 사용.
 */
@Service
public class RedisService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    // string (opsForValue)
    public void setStringOps(String key, String value, long ttl, TimeUnit unit){
        redisTemplate.opsForValue().set(key, value, ttl, unit);
    }

    public String getStringOps(String key){
        return (String) redisTemplate.opsForValue().get(key);
    }

    // list (opsForList)
    public void setListOps(String key, List<String> values){
        redisTemplate.opsForList().rightPushAll(key, values);
    }

    public List<String> getListOps(String key){
        Long len = redisTemplate.opsForList().size(key);
        return len == 0 ? new ArrayList<>() : redisTemplate.opsForList().range(key, 0, len-1);
    }

    // hash (opsForHash)
    public void setHashOps(String key, HashMap<String, String> value){
        redisTemplate.opsForHash().putAll(key, value);
    }

    public String getHashOps(String key, String hashKey){
        return redisTemplate.opsForHash().hasKey(key, hashKey) ? (String) redisTemplate.opsForHash().get(key, hashKey) : new String();
    }

    // set (opsForSet)
    public void setSetOps(String key, String... values){
        redisTemplate.opsForSet().add(key, values);
    }

    public Set<String> getSetOps(String key){
        return redisTemplate.opsForSet().members(key);
    }

    // sorted set (opsForZSet)
    public void setSortedSetOps(String key, List<Struct.SortedSet> values){
        for(Struct.SortedSet v : values){
            redisTemplate.opsForZSet().add(key, v.getValue(), v.getScore());
        }
    }

    public Set getSortedSetOps(String key){
        Long len = redisTemplate.opsForZSet().size(key);
        return len == 0 ? new HashSet<String>() : redisTemplate.opsForZSet().range(key, 0, len-1);
    }

}

# Junit 테스트

1. value - string

    @Test
    void string_redis() {
        String key = "string_redis";
        String value = "string";
        redisService.setStringOps(key, value, 10, TimeUnit.SECONDS);
        log.error("### Redis Key => {} | value => {}", key, redisService.getStringOps(key));
    }

- 서비스 로그

2021-04-07 19:52:43.926 ERROR 40256 --- [           main] com.code.redis.RedisApplicationTests     : ### Redis Key => string_redis | value => string

- 레디스 클라이언트

// 레디스 클라이언트 접속
$redis-cli
// 모든키 조회
127.0.0.1:6379> keys *
1) "string_redis"
// 키 값 조회
127.0.0.1:6379> get string_redis
"string"
// 10초뒤 모든키 조회시 없다고 나옴
127.0.0.1:6379> keys *
(empty list or set)

2. value - list

    @Test
    void list_redis() {
        String key = "list_redis";
        List<String> values = new ArrayList<>();
        values.add("list_01");
        values.add("list_02");
        redisService.setListOps(key, values);
        log.error("### Redis Key => {} | value => {}", key, redisService.getListOps(key));
        
        String keyEty = "empty_key";
        log.error("### Redis Empty Key => {} | value => {}", keyEty, redisService.getListOps(keyEty));
    }

- 서비스 로그

2021-04-07 19:58:27.180 ERROR 40357 --- [           main] com.code.redis.RedisApplicationTests     : ### Redis Key => list_redis | value => [list_01, list_02]
2021-04-07 19:58:27.182 ERROR 40357 --- [           main] com.code.redis.RedisApplicationTests     : ### Redis Empty Key => empty_key | value => []

- 레디스 클라이언트

// 모든키 조회
127.0.0.1:6379> keys *
1) "list_redis"
// 키 값 가져오기 (저장하는 값에 따라 값을 가져오는 명령어가 달라 에러 발생)
127.0.0.1:6379> get list_redis
(error) WRONGTYPE Operation against a key holding the wrong kind of value
// 처음부터 끝까지 키의 값 가져오기
// lrange key 시작인덱스 끝인덱스(끝 인덱스는 결과에 포함됨)
127.0.0.1:6379> lrange list_redis 0 -1
1) "list_01"
2) "list_02"

3. value - hashmap

    @Test
    void hash_redis() {
        String key = "hash_redis";

        HashMap<String, String> map = new HashMap<>();
        String mapKeyOne = "hash_key_1";
        String mapKeyTwo = "hash_key_2";

        map.put(mapKeyOne, "hash_value_1");
        map.put(mapKeyTwo, "hash_value_2");

        redisService.setHashOps(key, map);
        log.error("### Redis One Key => {} | value => {}", key, redisService.getHashOps(key, mapKeyOne));
        log.error("### Redis Two Key => {} | value => {}", key, redisService.getHashOps(key, mapKeyTwo));

        String keyEty = "key_empty";
        log.error("### Redis Empty hash Key => {} | value => {}", keyEty, redisService.getHashOps(key, keyEty));
        log.error("### Redis Empty Key => {} | value => {}", keyEty, redisService.getHashOps(keyEty, mapKeyOne));
    }

- 서비스 로그 

2021-04-07 20:08:04.932 ERROR 40501 --- [           main] com.code.redis.RedisApplicationTests     : ### Redis One Key => hash_redis | value => hash_value_1
2021-04-07 20:08:04.933 ERROR 40501 --- [           main] com.code.redis.RedisApplicationTests     : ### Redis Two Key => hash_redis | value => hash_value_2
2021-04-07 20:08:04.933 ERROR 40501 --- [           main] com.code.redis.RedisApplicationTests     : ### Redis Empty hash Key => key_empty | value => 
2021-04-07 20:08:04.933 ERROR 40501 --- [           main] com.code.redis.RedisApplicationTests     : ### Redis Empty Key => key_empty | value => 

- 레디스 클라이언트

// 모든 키 조회
127.0.0.1:6379> keys *
1) "hash_redis"
2) "list_redis"
// 해시 값 가져오기
127.0.0.1:6379> hget hash_redis hash_key_1
"hash_value_1"
127.0.0.1:6379> hget hash_redis hash_key_2
"hash_value_2"

4. value - set

    @Test
    void set_redis() {
        String key = "key_set";
        
        redisService.setSetOps(key, "value_1","value_2","value_1");
        log.error("### Redis Key => {} | value => {}", key, redisService.getSetOps(key));

        String keyEty = "key_empty";
        log.error("### Redis Empty Key => {} | value => {}", keyEty, redisService.getSetOps(keyEty));
    }

- 서비스 로그 (중복된 값은 저장하지 않는다)

2021-04-07 20:20:21.711 ERROR 40675 --- [           main] com.code.redis.RedisApplicationTests     : ### Redis Key => key_set | value => [value_2, value_1]
2021-04-07 20:20:21.714 ERROR 40675 --- [           main] com.code.redis.RedisApplicationTests     : ### Redis Empty Key => key_empty | value => []

- 레디스 클라이언트

// 모든 키 가져오기
127.0.0.1:6379> keys *
1) "hash_redis"
2) "key_set"
3) "list_redis"
// 키 값 가져오기 (set)
127.0.0.1:6379> smembers key_set
1) "value_2"
2) "value_1"

5. value - sorted set

    @Test
    void sortedSet_redis() {
        String key = "sortedSet_redis";

        List<Struct.SortedSet> values = new ArrayList<>();
        values.add(new Struct.SortedSet(){{
            setValue("sortedSet_value_100");
            setScore(100D);
        }});
        values.add(new Struct.SortedSet(){{
            setValue("sortedSet_value_10");
            setScore(10D);
        }});
        redisService.setSortedSetOps(key, values);
        log.error("### Redis Key => {} | value => {}", key, redisService.getSortedSetOps(key));

        String keyEty = "key_empty";
        log.error("### Redis Empty Key => {} | value => {}", keyEty, redisService.getSortedSetOps(keyEty));
    }

- 서비스 로그 (오름차순)

2021-04-07 20:27:01.875 ERROR 40785 --- [           main] com.code.redis.RedisApplicationTests     : ### Redis Key => sortedSet_redis | value => [sortedSet_value_10, sortedSet_value_100]
2021-04-07 20:27:01.876 ERROR 40785 --- [           main] com.code.redis.RedisApplicationTests     : ### Redis Empty Key => key_empty | value => []

- 레디스 클라이언트

// 모든 키 조회
127.0.0.1:6379> keys *
1) "hash_redis"
2) "key_set"
3) "list_redis"
4) "sortedSet_redis"
// 키 값 가져오기 (sorted set)
127.0.0.1:6379> zrange sortedSet_redis 0 -1
1) "sortedSet_value_10"
2) "sortedSet_value_100"

# 참고 사항

- Q) 데이터를 중복으로 저장시 어떻게 저장할까?

- string : overwrite
- list : tail add
- hash : overwrite
- set : 중복된 값이 있으면 저장 안함, 중복 되지 않으면 순서 상관없이 저장
- sorted set : 중복된 값이 있으면 score 만 업데이트하여 오름차순 정렬, 중복 되지 않으면 score 오름차순으로 저장

 

- Q) 어떤 라이브러리가 나을까? - lettuce

:: 친절하게 테스트 진행한 부분을 정리한 블로그를 보았다...! (테스트 할 필요없다 앗싸!)

:: jojoldu.tistory.com/418

- Q) redis 서버 장애시 각 라이브러리는 어떻게 처리할까?

lettuce jedis
# 서비스 중단
$ brew services stop redis

- 증상
1. 중단 즉시 : ConnectionWatchdog class 가 Redis Node가 죽는걸 감지하고 30초 간격으로 connecting 을 계속 시도 함 (AutoReconnect 를 통해 다시 커넥션을 맺는다.)

2. 레디스 서비스 호출시 : 장애시 서비스 호출하면 바로 오류 리턴하지 않고 설정한 시간 동안 redis 연결시도를 계속 하고, 그 안에 복구 안되면 오류 리턴 (아래 정리)

3. redis 장애 복구시 : '30초 뒤에 모니터링에 정상 감지' or  '서비스 호출' 시 재 연결됨
# 서비스 중단
$ brew services stop redis

- 증상
1. 중단 즉시 : lettuce 처럼 connecting 계속 시도 하지는 않음 (사용시에만 연결 체크함 - 장애 복구시 바로 연결하여 값 리턴)

2. 레디스 서비스 호출시 : 장애시 서비스 호출하면 연결 재 시도 없이 바로 오류 리턴(타임 아웃 설정해도 동일)
- JedisConnectionException

3. redis 장애 복구시 : '서비스 호출' 시 재 연결됨

-> lettuce 중단시 라이브러리에서 남기는 서비스 로그 (모니터링)

2021-04-07 21:32:02.912  INFO 41946 --- [xecutorLoop-1-5] i.l.core.protocol.ConnectionWatchdog     : Reconnecting, last destination was localhost/<unresolved>:6379
2021-04-07 21:32:02.914  WARN 41946 --- [ioEventLoop-6-6] i.l.core.protocol.ConnectionWatchdog     : Cannot reconnect to [localhost/<unresolved>:6379]: Connection refused: localhost/127.0.0.1:6379
2021-04-07 21:32:33.014  INFO 41946 --- [xecutorLoop-1-5] i.l.core.protocol.ConnectionWatchdog     : Reconnecting, last destination was localhost/<unresolved>:6379
2021-04-07 21:32:33.016  WARN 41946 --- [ioEventLoop-6-7] i.l.core.protocol.ConnectionWatchdog     : Cannot reconnect to [localhost/<unresolved>:6379]: Connection refused: localhost/127.0.0.1:6379
2021-04-07 21:33:03.110  INFO 41946 --- [ecutorLoop-1-10] i.l.core.protocol.ConnectionWatchdog     : Reconnecting, last destination was localhost/<unresolved>:6379
2021-04-07 21:33:03.113  WARN 41946 --- [ioEventLoop-6-8] i.l.core.protocol.ConnectionWatchdog     : Cannot reconnect to [localhost/<unresolved>:6379]: Connection refused: localhost/127.0.0.1:6379

-> lettuce 중단시 라이브러리에서 남기는 서비스 로그 (호출시)

참고 :: Aspect 는 제가 만든 class 지만, 뒤에 남은 에러 메세지는 라이브러리에서 남기는 메세지 리턴한 부분입니다

2021-04-07 23:10:32.694 ERROR 44533 --- [nio-8081-exec-9] com.code.redis.aspect.RedisCacheAspect   : ###  [RedisCacheAspect] Error : Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException: Command timed out after 1 second(s)

 

-> lettuce 장애 복구시 라이브러리에서 남기는 서비스 로그 (모니터링 or 호출시)

2021-04-07 21:35:03.508  INFO 41946 --- [xecutorLoop-1-6] i.l.core.protocol.ConnectionWatchdog     : Reconnecting, last destination was localhost/<unresolved>:6379
2021-04-07 21:35:03.514  INFO 41946 --- [oEventLoop-6-12] i.l.core.protocol.ReconnectionHandler    : Reconnected to localhost/<unresolved>:6379

 

-> jedis 장애시 라이브러리에서 남기는 서비스 로그 (호출시)

참고 :: Aspect 는 제가 만든 class 지만, 뒤에 남은 에러 메세지는 라이브러리에서 남기는 메세지 리턴한 부분입니다

// 컨넥션 풀을 설정 안했을시
2021-04-07 21:42:08.760 ERROR 42837 --- [nio-8081-exec-9] com.code.redis.aspect.RedisCacheAspect   : ###  [RedisCacheAspect] Error : Cannot get Jedis connection; nested exception is redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool

// 컨넥션 풀을 설정 했을시
2021-04-07 23:34:35.185 ERROR 45545 --- [nio-8081-exec-8] com.code.redis.aspect.RedisCacheAspect   : ###  [RedisCacheAspect] Error : Unexpected end of stream.; nested exception is redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.

 

- > jedis 장애 복구시 서비스 로그

없음

# 참고 설정

- lettuce 장애시 서비스 호출하면 연결 시간(재시도 시간) 설정

// 위에 빈 설정 했던 부분을 아래와 같이 변경
@Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(redisProperties.getHost(), redisProperties.getPort());
        LettuceConnectionFactory factory = new LettuceConnectionFactory(config);
        factory.setTimeout(1000); // 이부분으로 millisecond 단위 (1000ms = 1s)
        return factory;
    }

- jedis connection pool 설정 및 연결 시간 설정

// jedis 사용시 해당 빈 정의 필요
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        JedisConnectionFactory factory = new JedisConnectionFactory(new RedisStandaloneConfiguration(redisProperties.getHost(), redisProperties.getPort()));
        factory.setTimeout(3000);

        // jedis pool
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(100); // 최대로 열수 있는 connection 수가 100개
        jedisPoolConfig.setMaxIdle(10); // 바로 사용할 수 있는 게 10개
        factory.setUsePool(true);
        factory.setPoolConfig(jedisPoolConfig);

        return factory;
    }

 

Q) redis 장애를 대비할 수 있는 방법이 있을까?

- redis sentinel 이용! (다음 블로그에 계시 예정)

728x90