티스토리 뷰

개요

  • Redis는 바이너리 데이터 저장에 최적화된 인메모리 Key-Value 스토어로 RDBMS를 제외한 데이터 저장소 중 가장 유명하고 널리 쓰이고 있다. 캐시 저장소로 Redis를 적절히 활용하면 API 응답 시간을 10ms 수준으로 낮출 수 있다.

목표

  • Spring Boot 기반으로 Redis 저장소에 대한 CRUD를 수행할 수 있다.
  • Java 오브젝트를 바이너리 변환된 JSONMessagePack 형식으로 Redis 저장소에 저장할 수 있다.

라이브러리 종속성 추가

프로젝트 루트의 build.gradle에 아래 내용을 추가한다.

dependencies {
    compile 'org.springframework.data:spring-data-redis'
    compile group: 'redis.clients', name: 'jedis', version: '2.9.0'
    compile group: 'org.msgpack', name: 'jackson-dataformat-msgpack', version: '0.8.13'
}
  • jackson-dataformat-msgpack 아티팩트는 MessagePack 형식을 지원하기 위한 Jackson의 확장 라이브러리이다. MessagePackJSON 문자열의 내용은 그대로 유지하면서 바이너리 형식에 최적화한 것으로 Redis와 같은 바이너리 저장소에 특화된 형식이다. 저장소의 메모리 공간을 절약할 수 있다.

@Configuration 클래스 작성

package com.jsonobject.example.config;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;

@Configuration
public class RedisConfig {

    @Bean("jedisConnectionFactory")
    public JedisConnectionFactory jedisConnectionFactory() {

        JedisConnectionFactory factory = new JedisConnectionFactory();
        factory.setHostName({HOST_NAME});
        factory.setPort({PORT});
        factory.setDatabase({DATABASE_NO});
        factory.setPassword({PASSWORD});
        factory.setTimeout({TIME_OUT_MS});
        factory.setUsePool(true);

        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal({MAX_CONNECTION_POOL_COUNT});
        poolConfig.setTestWhileIdle(true);
        poolConfig.setNumTestsPerEvictionRun(-1);
        poolConfig.setTimeBetweenEvictionRunsMillis(5000);
        poolConfig.setBlockWhenExhausted(true);
        poolConfig.setMaxWaitMillis({TIME_OUT_MS});
        factory.setPoolConfig(poolConfig);

        return factory;
    }

    @Bean("stringRedisTemplate")
    public StringRedisTemplate stringRedisTemplate(@Qualifier("jedisConnectionFactory") JedisConnectionFactory jedisConnectionFactory) {

        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(jedisConnectionFactory);

        return template;
    }

    @Bean("messagePackRedisTemplate")
    public RedisTemplate<String, byte[]> messagePackRedisTemplate(@Qualifier("jedisConnectionFactory") JedisConnectionFactory jedisConnectionFactory) {

        RedisTemplate<String, byte[]> template = new RedisTemplate<>();
        template.setConnectionFactory(jedisConnectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setEnableDefaultSerializer(false);

        return template;
    }

    @Bean("messagePackObjectMapper")
    public ObjectMapper messagePackObjectMapper() {

        return new ObjectMapper(new MessagePackFactory()).registerModule(new JavaTimeModule()).disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    }
}
  • JedisJava 플랫폼에서 가장 신뢰할 수 있는 경량의 Redis 클라이언트 라이브러리이지만 설계 구조상 Thread Safety가 보장되지 않는다. 따라서 웹 애플리케이션 같은 멀티 쓰레드 기반의 애플리케이션에서는 커넥션 풀을 사용해야 Thread Safety를 보장 받을 수 있다.
  • Redis는 기본적으로 Key, Value를 바이트의 배열로 저장한다. StringRedisTemplate를 사용하면 Key, Value 모두 문자열로 저장할 수 있다.
  • RedisTemplateRedis 저장소에 오브젝트를 저장할 때 기본값으로 정의된 JdkSerializationRedisSerializer을 이용한다. 따라서 해당 오브젝트는 반드시 java.io.Serializable 인터페이스를 구현해야 한다. 이 방식의 문제점은 다른 언어 환경에서 Redis 저장소에 접근할 경우 값을 인식하지 못한다는 것이다. 또한 오브젝트의 클래스 메타 정보를 저장하다보니 크기 또한 커진다. 특정 언어에 종속시키지 않으려면 저장되는 값으로 JSON 문자열 또는 MessagePack 형식을 고려해야 한다. 이를 위해 messagePackObjectMappermessagePackRedisTemplate 빈을 작성했다.

@Repository 클래스 작성

  • 아래는 토큰 오브젝트를 MessagePack 형식으로 Redis 저장소에 저장하고 불러오는 예제이다.
@Repository
public class TokenDAO {

    @Autowired
    @Qualifier("messagePackRedisTemplate")
    private RedisTemplate<String, byte[]> messagePackRedisTemplate;

    @Autowired
    @Qualifier("messagePackObjectMapper")
    private ObjectMapper messagePackObjectMapper;

    public void createToken(Token token) {

        messagePackRedisTemplate.opsForValue().set("token:" + token.getToken(), messagePackObjectMapper.writeValueAsBytes(accessToken));
    }

    public Token getToken(String token) {

        return messagePackObjectMapper.readValue(messagePackRedisTemplate.opsForValue().get("token:" + token, Token.class);
    }
}
  • createToken()을 통해 앞서 정의된 messagePackObjectMapper 빈을 이용하여 Java POJO 오브젝트를 플랫폼 중립적인 순수한 MesssagePack 형식으로 저장한다. JSON 문자열 대비 2/3 절약된 크기로 오브젝트를 저장할 수 있으며 어떤 언어에서도 해당 값에 접근하고 해석할 수 있다.

  • 마찬가지로 getToken()을 통해 저장된 MessagePack 값을 오브젝트로 변환하여 불러온다.

일치하는 패턴의 Key 이름 조회

  • Redis는 초고속의 Key-Value 저장소로서 조회 및 검색 관점에서 RDBMS와 비교하면 제공하는 기능이 매우 단순하고 제한적이다.
  • 특히 특정 패턴과 일치하는 Key 이름을 조회하는 것은 성능 문제로 운영 환경에서 가장 지양해야 하지만 불가피하게 필요할 때가 있다.
  • 가장 먼저 아래는 StringRedisTemplate을 이용하여 KEYS 명령을 수행하는 예제이다. USER_ID:로 시작하는 Key 이름을 한 번에 조회한다. KEYS 명령이 실행되면 응답이 완료되기 까지 다른 요청은 모두 블록되기 때문에 운영 환경에서 절대 사용을 자제해야할 안티 패턴으로 취급된다.
// KEYS "USER_ID:*"
Set<String> keys = stringRedisTemplate.keys("USER_ID:*");
Iterator<String> it = redisKeys.iterator();
while (it.hasNext()) {
    System.out.println(it.next()); // 조회된 Key의 이름을 출력
}
  • 아래는 SCAN 명령을 수행하여 위와 동일한 결과를 출력하는 예제이다. SCAN 명령은 커서를 기반으로 부분적으로 데이터를 조회하기 때문에 KEYS보다는 운영 환경에 미치는 부담이 상대적으로 적다.
// SCAN 0 MATCH "USER_ID:*" COUNT 10
RedisConnection redisConnection = null;
try {
    redisConnection = stringRedisTemplate.getConnectionFactory().getConnection();
    ScanOptions options = ScanOptions.scanOptions().match("USER_ID:*").count(10).build();
    Cursor<byte[]> cursor = redisConnection.scan(options);
    while (cursor.hasNext()) {
        System.out.println(new String(cursor.next())); // 조회된 Key의 이름을 출력
    }
} finally {
    redisConnection.close();
}

트러블슈팅: Could not get a resource from the pool

[Message] Cannot get Jedis connection; nested exception is redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
[Exception] org.springframework.data.redis.RedisConnectionFailureException
[Root Exception] java.net.ConnectException
  • 2가지 경우에 발생한다. 첫째, Redis 인스턴스가 어떠한 문제로 중단되어 연결이 불가능한 경우이다. 둘째, 설정된 최대 동시 접속 허용 클라이언트 수(maxclients)를 초과할 경우 발생한다. 해결책은 후자의 경우 서버와 클라이언트 모두 적절한 클라이언트 수 제한이 필요하다.
  • 서버 측인 Redis 인스턴스에서는 아래와 같이 설정할 수 있다.
$ nano /etc/redis.conf
maxclients 10000 // (기본값 10000)
  • 클라이언트에서는 아래와 같이 설정할 수 있다.
JedisPoolConfig poolConfig = new JedisPoolConfig();

// 커넥션 풀의 최대 생성 연결 값을 설정한다. (기본값은 8)
poolConfig.setMaxTotal(1000);

// 커넥션 풀이 가득 찼을 경우 준비된 연결이 도착하기를 기다린다.
poolConfig.setBlockWhenExhausted(true);

// 커넥션 풀이 가득 찼을 경우 새로운 연결을 기다리지 않고 NoSuchElementException 예외를 발생시킨다.
poolConfig.setBlockWhenExhausted(false);

JedisConnectionFactory factory = new JedisConnectionFactory();
...
factory.setUsePool(true);
factory.setPoolConfig(poolConfig);

트러블슈팅: OOM command not allowed when used memory > ‘maxmemory’.

[Message] JedisDataException: OOM command not allowed when used memory > 'maxmemory'.
[Exception] org.springframework.dao.InvalidDataAccessApiUsageException
[Root Exception] redis.clients.jedis.exceptions.JedisDataException
  • Redis 인스턴스에 대한 모든 쓰기 동작 실행시 발생하는 오류이다. Redis 인스턴스에 저장된 데이터 양이 설정된 메모리 한계치(maxmemory)에 도달하여 더이상 쓰기 행위가 불가능하기 때문에 발생한다.
  • 서버 측인 Redis 인스턴스에서 아래와 같이 변경할 수 있다.
$ nano /etc/redis.conf
maxmemory 1gb // 메모리 한계치, 기본값은 0으로 무제한
maxmemory-policy volatile-lru // 한계치 도달시 데이터 삭제 정책 설정, 기본값은 volatile-lru로 만기 시점을 가지는 키에 한하여 읽기 동작이 가장 오래 전에 발생한 순서로 제거
  • 해당 오류를 예방하는 가장 효과적인 조치는 maxmemory 값을 충분히 늘리는 것이다. 2차 조치는 maxmemory-policy를 수정하는 것이다. noeviction은 한계치 도달시 어떠한 행위도 하지 않기 때문에 위 오류가 발생한다. allkeys-lru는 읽기 동작이 가장 오래 전에 발생한 키를 삭제하여 저장 공간을 확보하므로 위 오류가 발생할 확률이 줄어든다. [관련 링크]

트러블슈팅: LOADING Redis is loading the dataset in memory

[Message] JedisDataException: LOADING Redis is loading the dataset in memory
[Exception] org.springframework.dao.InvalidDataAccessApiUsageException
[Root Exception] redis.clients.jedis.exceptions.JedisDataException
  • Redis 인스턴스 재시작 시점에 모든 읽기 동작 실행시 발생하는 오류이다. Redis 인스턴스가 기동되면서 RDB/AOF 파일로부터 데이터를 로딩하는 중이라 요청 수행이 불가능할 때 발생한다. 시간이 지나면 응답이 가능하므로 엄밀히 말하면 오류는 아니다.
  • Redis 저장소는 RDB/AOF 파일을 이용하여 메모리 상의 데이터를 디스크에 보관하여 인스턴스를 재시작하더라도 영구적으로 저장된 데이터를 유지할 수 있다. 만약 완전한 비휘발성 저장소로 사용하고자 한다면 아래와 같이 설정할 수 있다.
$ nano /etc/redis.conf
appendonly no
#save 900 1
#save 300 10
#save 60 10000
save ""

Redis 저장소 상태 확인 및 초기화

  • INFO 명령은 현재 Redis 저장소의 상태를 확인할 수 있는 유용한 정보를 제공한다. 짧은 시간 주기로 INFO 명령의 응답 결과를 로그로 남기면 사전 장애 예방 및 처리에 많은 도움을 받을 수 있다. 유명한 로그 기반 모니터링 도구들은 모두 이러한 방식을 사용한다. [관련 링크1]
> INFO
connected_clients:11 # 현재 접속 중인 클라이언트 개수
used_memory:2893651360 # 사용 메모리 bytes
maxmemory:4294967296 # 사용 가능한 최대 메모리 bytes
total_connections_received:87
total_commands_processed:456025 # 처리한 명령 개수
expired_keys:96 # 만료되어 삭제된 키 개수
evicted_keys:0 # maxmemory 제한으로 삭제된 키 개수
keyspace_hits:91979 # 성공한 키 조회 개수
keyspace_misses:28 # 실패한 키 조회 개수
db1:keys=4745407,expires=4745407,avg_ttl=872524771 # keys=전체 키 수
  • CONFIG RESETSTAT 명령은 INFO STATS에 노출되는 정보들을 초기화해준다. 짧은 시간 주기로 로그를 남기면서 매번 초기화해주면 특정 주기에 대한 유의미한 통계 로그를 획득할 수 있다.
> CONFIG RESETSTAT
OK
  • 명령 실행시 ERR unknown command ‘CONFIG’ 오류가 발생한다면 해당 계정에 권한이 없는 것이다. /etc/redis.conf 파일의 아래 부분을 주석 처리 해야 한다.
# rename-command CONFIG ""

참고 글

댓글
댓글쓰기 폼