Redis command ( api )

java RedisTemplate api도 hset 등으로 제공되고 있어 봐두는 것이 좋다.

 

redis 구조는 key --1:n-- field --1:1-- value

HSET / HGETHGETALL 을 보면 알 수 있는데

redis는 하나의 key에 여러개의 field-value가 연결될 수 있다.

```java

redis> HSET myhash field1 "Hello"
redis> HSET myhash field2 "World"

 

// in java https://lettuce.io/lettuce-4/release/api/com/lambdaworks/redis/api/async/RedisHashAsyncCommands.html

hmset(String key, Map<field, value> fields)

hset(K key, K field, V value)

```

그래서 hmset 사용 시 해당 key에 대해서, field명이 이미 있다면 overwrite, 없다면 해당 필드가 추가된다.

 

HSETNX는 not exist인 경우에만 동작하고 exist인 경우 no effect.

 

spring-boot-starter-data-redis-reactive

연동

build.gradle.kts

```kt

implementation("org.springframework.boot:spring-boot-starter-data-redis-reactive")

```

 

AutoConfiguration

```java

@ConfigurationProperties(prefix = "spring.redis")    // spring.redis 이하 properties들을 불러온다.

public class RedisProperties {

    private String host = "localhost";

    private int port = 6370;

    ...

```

  • RedisAutoConfiguration.java에서는 다음 두 가지 Bean을 미리 만들어서 제공하고 있음
    • ``kt StringRedisTemplate``
      • key, value, hashKey, hashValue의 Serializer로 ``kt RedisSerializer.string()`` 사용
    • ``kt RedisTemplate<Object, Object>``
      • key, value, hashKey, hashValue의 defaultSerializer로 `` JdkSerializationRedisSerializer`` 사용
      • 결국 Java Obj로 변환한다는 건데... 이는 여러모로 단점이 있음. ([Effective Java] 12장 직렬화 참고)
      • 그래서 Jackson을 Serializer로 사용하는 `` RestTemplate``을 따로 정의해서 사용하는 것이 좋아 보인다.

 

Auto Configuration Disable?
  • ``kt RedisReactiveAutoConfiguration::class``는 exclude 가능.
  • 반면 ``kt RedisAutoConfiguration::class``는 exclude하면 factory까지 생성이 안돼서 exclude 불가.

 

예제

 

더보기

```kt

@Configuration
class RedisConfig { // 방법 1, 3 공통
    @Bean
    fun reactiveRedisTemplate(factory: ReactiveRedisConnectionFactory, objectMapper: ObjectMapper): ReactiveRedisTemplate<String, Any> {


        val stringSerializer = StringRedisSerializer()
        val jsonSerializer = Jackson2JsonRedisSerializer(Any::class.java)
        jsonSerializer.setObjectMapper(objectMapper)
        val context = RedisSerializationContext.newSerializationContext<String, Any>()
                .key(stringSerializer)
                .value(jsonSerializer)
                .hashKey(stringSerializer)
                .hashValue(jsonSerializer)
                .build()
        return ReactiveRedisTemplate(factory, context)
    }

 

    // StringTemplate은 안해도 된다. RedisReactiveAutoConfiguration.java 에서 설정하고 있음.
    @Bean
    fun reactiveStringRedisTemplate(
            reactiveRedisConnectionFactory: ReactiveRedisConnectionFactory
    ): ReactiveStringRedisTemplate {


        return ReactiveStringRedisTemplate(reactiveRedisConnectionFactory)
    }

    /**
     * 방법 1. 이런 식으로 data class 마다 Bean을 직접 만든다. 완전 비추.
     */
    @Bean
    fun coffeeRedisOperations(factory: ReactiveRedisConnectionFactory): ReactiveRedisOperations<String, Coffee> {


        val builder = RedisSerializationContext.newSerializationContext<String, Coffee>(StringRedisSerializer())
        val context = builder
                .value(Jackson2JsonRedisSerializer(Coffee::class.java))
                .build()

        return ReactiveRedisTemplate(factory, context)
    }
}

/**
 * 방법 2. 각 서비스 클래스에서 getRedisTemplate을 호출해서 해당 타입의 RedisTemplate을 반환 받는 방법
 */
@Component
class RedisTemplateFactory(
        val connectionFactory: ReactiveRedisConnectionFactory, val objectMapper: ObjectMapper
) {


    final inline fun <reified V> getRedisTemplate(): ReactiveRedisTemplate<String, V> {
        val stringSerializer = StringRedisSerializer()
        val jsonSerializer = Jackson2JsonRedisSerializer(V::class.java)
        jsonSerializer.setObjectMapper(objectMapper)
        val context = RedisSerializationContext.newSerializationContext<String, V>()
                .key(stringSerializer)
                .value(jsonSerializer)
                .hashKey(stringSerializer)
                .hashValue(jsonSerializer)
                .build()
        return ReactiveRedisTemplate(connectionFactory, context)
    }
}

/**
 * 방법 3. RestTemplate과 비슷하게 사용하기 위해서...? 내부적으로는 Any로 처리하고 set, get해서 내려줄 때는 Casting해서 내려주는 방법.

 * 단점은 set, get 뿐만 아니라 다양한 메서드(keys, expire 등)를 모두 wrapping 해주어야 한다는 점.
 */
@Component
class RedisComponent(val redisTemplate: ReactiveRedisTemplate<String, Any>, val objectMapper: ObjectMapper) {


    fun <V> set(key: String, value: V): Mono<Boolean> {
        return redisTemplate.opsForValue().set(key, value as Any)
    }

    /**
     * 방법 3-1.
     * opsForValue().get()으로 Object를 받고 바깥에서 ObjectMapper.convertValue()를 사용해 V로 변환
     */
    fun <V> get(key: String, clazz: Class<V>): Mono<V> {
        return redisTemplate.opsForValue().get(key)
                .map { obj -> objectMapper.convertValue(obj, clazz) }
    }

    /**
     * 방법 3-2.
     * 아예 RedisTemplate 자체를 저수준부터 구현. (RestTemplate 처럼 메서드 기반으로.)
     */
}

```

 

방법2가 제일 괜찮아 보임.

 

Spring Data Redis API

  • low-level API는 `` RedisConnection`` 계열. binary 통신 제공
    • `` RedisClusterConnection``
  • 이를 고수준으로 추상화한 API는 `` RedisTemplate`` 계열. 객체 수준으로 다룰 수 있음
    • `` ReactiveRedisTemplate``
    • RedisTemplate은 클래스이고, 보통 주고 받을 때는 `` RedisOperations``라는 인터페이스를 사용한다
    • ``kt RedisOperations.opsForValue()`` 요런 식으로 있다고 보면 됨
  • (아래) Template = connection + serializer 정도로 생각하면 된다.

```kt

@Bean
fun redisOperations(factory: ReactiveRedisConnectionFactory): ReactiveRedisOperations<String, Coffee> {
    val builder = RedisSerializationContext.newSerializationContext<String, Coffee>(StringRedisSerializer())
    val context = builder.value(Jackson2JsonRedisSerializer(Coffee::class.java)).build()
    return ReactiveRedisTemplate(factory, context)
}

```

 

Redis Cluster

 

Redis도 DB처럼 Mapper 사용해서 CRUD하기?

class 이름 변경, 패키지 변경 시 매우 주의!! redis 해시에 _class 필드로 해당 클래스 이름(+패키지 경로)가 들어가기 때문!!

마찬가지로 필드명 변경도 주의해야 한다.

 

```kt

@Repository

interface AsyncWithdrawItemRepository: CrudRepository<AsyncWithdrawItem, String>

// 이런 구조의 장점은, mocking 하기 쉽다는 것이다.

class MockAsyncWithdrawItemRepository: AsyncWithdrawItemRepository {...}

```

 

 

@RedisHash & Repository 사용하는 방법 VS 방법2-RedisTemplateFactory 비교
  • reactive가 필요하거나, json serialize/deserialize 가 필요한 경우 : `` RedisTemplateFactory``
  • 메타 데이터나 설정 데이터 같은, 클래스는 존재하지만 개념적으로 해당 객체가 유니크한 경우(1:1) : `` RedisTemplateFactory``
  • Student 클래스 같이, 여러 인스턴스가 존재할 수 있으며 각각이 고유한 id 별로 식별되어야 하는 경우(1:n) : `` @RedisHash & Repository``
  • `` @RedisHash & Repository``는 redis 타입 hash, `` RedisTemplateFactory``는 json이므로 redis 타입 string

 

 

cli 명령어

```bash

redis/src/redis-server           # 서버 실행

redis/utils/install_server.sh    # 백그라운드 데몬으로 서버 실행

redis/src/redis-cli              # cli interaction

redis-cli -h {ip} -p {port}      # 원격지 접속

redis-cli -h {ip} -p {port} -c   # 클러스터에 접속할 때(타 클러스터에 있는 key-value도 조회 가능)

```

```bash

set {key} {value}

get {key}

keys {pattern}

FLUSHALL    # 변수 초기화

save

quit

shutdown    # redis-server terminate

```

 

docker 실행 명령어

```bash

$ docker run -p 6379:6379 -v redis-volume:/data --name my-redis -d --restart always redis redis-server --save 60 1 --loglevel warning --appendonly yes

```