Redis Replication은 하나의 Redis 서버(마스터)가 여러 개의 복제 서버(슬레이브)에게 데이터를 동기화하는 기능 

이를 통해 데이터의 가용성(Availability)과 부하 분산(Load Balancing) 효과를 얻을 수 있습니다.

 

주요 개념

✔ 마스터-슬레이브

  • 마스터 : 데이터를 쓰고, 슬레이브에게 변경 사항을 알림
  • 슬레이브 : 마스터의 데이터를 복제하여 여러 슬레이브를 통하여 읽기 요청을 분산 처리

✔ 비동기 복제

마스터가 데이터를 변경하면, 슬레이브는 이를 비동기적으로 복제함. 즉각적으로 일어나지는 않지만 성능이 뛰어나다.

 

✔  읽기 부하 분산

마스터 노드는 쓰기작업만을 하고 슬레이브 노드는 읽기만을 한다. 여러 노드를 통해 부하를 분산시킬 수 있다.

 

✔  페일오버

마스터 노드 장애발생 시, 슬레이브 노드 중 하나를 마스터 노드로 승격시키는 작업

 

 

윈도우 기준으로 C:\Program Files\Redis 해당 경로에서 redis.windows.conf 처럼 설정파일들을 마스터와 노드별로 만들어주어야 한다.

 

 

근데 웬만하면 Redis 버전은 5.0 이상으로 맞춰주는게 좋다. SpringBoot의 라이브러리중 lettuce라고 있는데 

이전 버전이 3.xx 이였다가 지원안하는 기능이 있어 다시 깔았다.

 

나는 마스터1 슬레이브노드2 구성으로 하였다. (실습차원에서 간단히만)

 

1. redis-master.conf 내용

port 6379
bind 0.0.0.0
protected-mode no
dir "C:\\redisdata\\master"
logfile C:\\redisdata\\master\\logs\\master.log
dbfilename "dump6379.rdb"

appendonly yes
appendfilename "appendonly-master.aof"

save 900 1
save 300 10
save 60 10000

# replica 관련
replica-read-only no

tcp-keepalive 60
timeout 0

 

 

port : 해당 마스터 노드는 6379 포트를 사용할 것이다.

bind : 연결 가능한 네트워크를 전체로 풀어줬다. (대량 요청 처리를 wsl에서 해야해서 개방함)

protected-mode no :  Redis 보안 보호 모드(Protected Mode)를 비활성화

기본적으로 **protected-mode는 yes**이며, bind 127.0.0.1이 아닐 경우 외부 접속을 차단합니다.

 

dir : Redis 데이터 파일(RDB 및 AOF)이 저장될 디렉터리 경로를 지정

appendonly: Redis의 AOF (Append-Only File) 영속성 기능을 활성화

 

 

2. redis-slave1.conf 내용

port 6380
bind 0.0.0.0
protected-mode no

dir "C:\\redisdata\\slave1"
logfile C:\\redisdata\\slave\\logs\\slave.log

dbfilename "dump6380.rdb"

appendonly yes
appendfilename "append-replica1.aof"

replicaof 127.0.0.1 6379
replica-read-only yes

tcp-keepalive 60
timeout 0

 

replcaof : 6379 노드에 대한 복제노드라고 설정해준다.

일반적으로 aof기능은 슬레이브 노드에서는 비활성화 해준다. 성능이 안나오기 때문

 

3. redis-slave2.conf 내용

port 6381
bind 0.0.0.0
protected-mode no

dir "C:\\redisdata\\slave2"
logfile C:\\redisdata\\slave2\\logs\\slave2.log

dbfilename "dump6381.rdb"

appendonly no
appendfilename "appendonly-replica2.aof"

replicaof 127.0.0.1 6379
replica-read-only yes

# redis와 클라이언트 간의 유휴상태 연결이 끊어지지 않고 유효한지 확인하기 위해 주기적으로 패킷을 60초마다 주고받음
tcp-keepalive 60
# 클라이언트가 Redis 서버에 연결된 상태에서 아무런 명령을 내리지 않아도 Redis 서버는 이 연결을 자동으로 끊지 않고 영원히 유지 , 0 이기때문에
timeout 0

 

 이제 start redis-server redis-master.conf 해당 명령어를 통하여 마스터 노드를 활성화한다.

(물론 Redis의 conf가 있는 경로까지 접근해주어야 한다.)

그러면 redis-server cmd 창이 하나 나온다.

같은 방법으로 start redis-server redis-slave1.conf ,  start redis-server redis-slave2.conf를

실행한다.

창이 2개가 더 나오고 마스터노드를 실행한 cmd에서는 connection 연결이 성공했다고 나올것이다.

 

만약 안되면 redis-server redis-slave1.conf 로 명령어 실행 ===> 안되면 에러 출력나옴

 

확인하는 명령어 : redis-cli -p 6380 info replication  : 실행하면 실행상태 나옴.

 

 

근데 이  창들은 입력이 되지 않는 상태이므로 cmd를 관리자권한으로 실행해서 하나 더 키자.

해당 명령어를 입력하면 마스터 노드에 접근이 가능하다.

 

 

슬레이브 노드 (6380) 에 접근하면 해당키의 값이 맞는것을 확인 가능하다.

 

이제 Replication이 정상 동작하는 것을 확인했으니 Sentinel을 설정하여보자.

 

 

Sentinel

Redis Sentinel은 고가용성과 자동 복구를 지원하는 모니터링 및 페일오버 시스템이다.

Sentinel을 사용하면 Redis 클러스터가 자동으로 장애 복구되며, 서비스 중단 없이 운영할 수 있다.

주요 개념

✔ 모니터링

마스터/슬레이브 상태를 지속적으로 확인

 

✔ 클라이언트 리디렉션

새로운 마스터를 클라이언트에 자동 안내

 

✔  페일오버

마스터 노드 장애발생 시, 슬레이브 노드 중 하나를 마스터 노드로 승격시키는 작업

 

마찬가지로 sentinel에 대한 conf 파일을 작성해준다.

 

1. sentinel.conf 내용

port 26379
bind 0.0.0.0
protected-mode no
dir "C:\\Program Files\\Redis"

sentinel myid 20976be56cfc8b19424579ca6d6609ac9272ee73
sentinel deny-scripts-reconfig yes
sentinel monitor mymaster 127.0.0.1 6379 1
sentinel down-after-milliseconds mymaster 5000
# Generated by CONFIG REWRITE
sentinel failover-timeout mymaster 10000
sentinel config-epoch mymaster 4
sentinel leader-epoch mymaster 4
sentinel known-replica mymaster 127.0.0.1 6380
protected-mode no
sentinel known-replica mymaster 127.0.0.1 6381
sentinel current-epoch 4

 

근데 이 내용은 업데이트 된 내용이다. 내가 업데이트 한건 아니고 sentinel관련 테스트를 하면서 redis에서 자동업데이트 한 것처럼 보인다.

 

sentinel myid : 각 Sentinel 인스턴스는 고유한 ID (myid)를 자동 생성하여 설정

 

sentinel deny-scripts-reconfig yes : Sentinel이 스크립트 기반 설정 변경을 금지

 

sentinel monitor mymaster 127.0.0.1 6379 1 : sentinel이 mymaster라는 이름의 마스터 노드를 감시 , 

마스터 장애 감지를 위해 필요한 Sentinel 투표 수: 1

만약 뒤에 숫자가 3이라면 3개의 Sentinel 감시노드가 감지를 해야 장애라고 인식한다. 여기서는 테스트 목적이니 하나로 둔다.

 

sentinel down-after-milliseconds mymaster 5000 : 마스터가 5000ms(5초) 동안 응답이 없으면 다운된 것으로 간주

 

sentinel failover-timeout mymaster 10000 :  Sentinel이 마스터 장애를 감지한 후, 새로운 마스터를 승격하는 최대 시간(10초)

 

sentinel known-replica mymaster 127.0.0.1 6380 : 센티넬이 마스터 밑에 어떤 레플리카들이 있는지 알고 있다는 기록.

sentinel known-replica mymaster 127.0.0.1 6381

 

 

 

sentinel config-epoch mymaster 4
sentinel leader-epoch mymaster 4
sentinel current-epoch 4

이 부분들은 자동 변경된 설정인듯 하다.

 

 

  • config-epoch: mymaster에 대한 Sentinel의 구성 버전
  • leader-epoch: Sentinel 클러스터 내에서 리더로 선정된 Sentinel의 버전
  • current-epoch: Sentinel 클러스터 전체에서 관리하는 버전

 

이제 마스터가 다운될시 슬레이브가 승격하는지 테스트를 해보자.

 

2 . redis-server.exe sentinel.conf --sentinel   

해당 명령어 실행 ,  --sentinel 을 뒤에 붙혀주지 않으면 실행이 안된다.

 

3. redis-cli -p 26379 sentinel master mymaster   :  Sentinel이 감지한 현재 마스터 확인

해당 명령어를 실행하면 6379 포트를 마스터라고 감지한 것을 알 수 있다.

or redis-cli -p 26379 INFO server 이걸로도 테스트 가능

 

 

 

4. redis-cli -p 6379 shutdown 명령어를 통해 마스터 노드 종료

5. redis-cli -p 26379 sentinel master mymaster 해당 명령어로 마스터를 조회해보면 6381로 바뀐것이 확인가능하다.

 

 

 

sentinel의 정상 작동을 확인했으니 다시 6379를 실행하여서 부하분산에 대한 성능 테스트를 해보자.

 

6. start redis-server redis-master.conf 실행

 

 

부하 분산 성능 테스트

1. springBoot 프로젝트에서 build.gradle 파일에 다음과 같이 의존성 추가

 

 

Lettuce는 비동기 및 동기 방식 모두 지원하는 Redis 클라이언트 라이브러리

Spring Boot에서 spring-data-redis가 Lettuce를 기본 클라이언트로 사용

 

Apache Commons Pool 2는 Lettuce의 커넥션 풀 관리에 사용됨

 

2. springBoot 프로젝트에서 application.properties 파일에 다음과 같이 설정 추가

 

캐시타입으로 REDIS 를 지정해주고 sentinel 노드에 대한 정보를 기입해준다.

spring.redis.sentinel.read-only-replicas.enabled 는 없어도 되는듯하다. 

그리고 lettuce를 통해 커넥션 풀 옵션 조정

 

* 이런 커넥션 풀을 통해 비록 레디스는 싱글스레드 이지만 웹 어플리케이션딴에서 레디스에 요청을 보내기 위해

블로킹 되는 상황을 막아준다. (레디스에 요청 밀어놓고 딴 행동을 할 수 있음. 스레드 고갈 위험 감소) Redis는 싱글 스레드로 명령을 처리하지만,
커넥션 풀을 사용하면 웹 애플리케이션에서
Redis 커넥션을 기다리느라 블로킹되는 상황을 줄일 수 있다.
그 결과 요청을 Redis로 미리 전달하고,
웹 서버 스레드 고갈 위험을 낮출 수 있다.

 

3. springBoot 프로젝트에서 RedisConfg 클래스 생성

package com.example.testsecurity2.config;

import io.lettuce.core.ReadFrom;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@Configuration
@EnableCaching
public class RedisConfig {


    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
                .master("mymaster")
                .sentinel("127.0.0.1", 26379); // Sentinel을 사용하여 자동 감지

        LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()
                .readFrom(ReadFrom.MASTER_PREFERRED) // 마스터에서 읽기 우선
                .commandTimeout(Duration.ofSeconds(5))
                .build();

        return new LettuceConnectionFactory(sentinelConfig, clientConfig);
    }

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        // 기본 Redis 캐시 설정을 정의
        RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(10)) // 캐시 TTL(Time To Live) = 10분 설정 (10분 후 데이터 자동 삭제)
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
                        new Jackson2JsonRedisSerializer<>(Object.class))) // 값 직렬화 방식 = Jackson을 사용한 JSON 직렬화
                .disableCachingNullValues(); // null 값은 캐싱하지 않도록 설정

        return RedisCacheManager.builder(redisConnectionFactory) // RedisConnectionFactory 기반으로 캐시 매니저 생성
                .cacheDefaults(cacheConfig) // 위에서 설정한 기본 캐시 구성 적용
                .build();
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory); // Redis 연결 팩토리 설정 (Redis와 연결)

        // Key와 HashKey(해시의 키)는 일반 문자열로 직렬화
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());

        // Value와 HashValue(해시의 값)은 JSON 형식으로 직렬화
        template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
        template.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));

        template.afterPropertiesSet(); // Bean 설정 완료 후 초기화
        return template;
    }


}

 

ReadFrom.MASTER_PREFERRED를 통하여 마스터 노드에서만 읽기로 설정 --> 부하 분산이 안되었을때를 측정하기 위해

 

4. springBoot 프로젝트에서 service클래스 생성

package com.example.testsecurity2.service;


import com.example.testsecurity2.entity.Post;
import com.example.testsecurity2.repository.PostRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StopWatch;

import java.util.Set;
import java.util.concurrent.TimeUnit;

@Service
@RequiredArgsConstructor
public class PostViewService {

    private final String POST_VIEW_KEY = "post:view";
    private final RedisTemplate<String, String> redisTemplate;
    private final PostRepository postRepository;


    // Redis Replication 측정을 위한 것, 마스터에서만 읽기
    public String getDataFromMaster(Long key) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        System.out.println("조회: " + key);
        String value = postRepository.findById(key).map(Post::getTitle).orElse("Null Value");

        stopWatch.stop();
        System.out.println("시간: " + stopWatch.getTotalTimeMillis() + " ms");

        return value;
    }
    //읽기 분산 처리
    @Cacheable(value = "testCache", key = "#key")
    public String getDataFromSlave(Long key) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        System.out.println("조회2: " + key);
        String value = postRepository.findById(key).map(Post::getTitle).orElse("Null Value");

        stopWatch.stop();
        System.out.println("시간2: " + stopWatch.getTotalTimeMillis() + " ms");

        return value;
    }
}

 

캐싱을 한것과 안한것에 대한 차이도 볼려고  두개의 메서드를 준비하였다. @Cacheable 을 설정한 메서드가 캐싱을 이용한다.

 

5. springBoot 프로젝트에서 api를 위한 Controller 생성

package com.example.testsecurity2.controller;


import com.example.testsecurity2.entity.Post;
import com.example.testsecurity2.repository.PostRepository;
import com.example.testsecurity2.service.PostViewService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/posts")
@RequiredArgsConstructor
public class PostController {


    private final PostRepository postRepository;
    private final PostViewService postViewService;



    @GetMapping("/nocache")
    public String getFromMaster(@RequestParam Long id) {
        return postViewService.getDataFromMaster(id);
    }


    @GetMapping("/cache")
    public String getFromSlave(@RequestParam Long id) {
        return postViewService.getDataFromSlave(id);
    }
}

 

wsl을 통해 lua 스크립트를 하나 작성해준다. (random 읽기를 위한 api 호출 스크립트)

 

- random_id.lua (레디스 캐시 테스트용)

math.randomseed(os.time())

request = function()
  local id = math.random(1, 10000)
  return wrk.format(
    "GET",
    "/posts/cache?id=" .. id
  )
  end

 

- random_id_nocache.lua (레디스 캐시 없이 그냥 일반 디스크 조회용)

math.randomseed(os.time())

request = function()
        local id = math.random(1, 10000)
        return wrk.format(
                "GET",
                "/posts/nocache?id=" .. id
        )
end

 

 

 

 

6. wsl을 통한 대량 요청으로 분산 부하 테스트

 

wsl을 실행하여 wrk 을 통하여 100번의 요청을 보낸다.

우선 레디스 캐시가 적용된 것과 적용없이 일반적인 디스크 조회를 비교해본다.

 

wrk -t4 -c100 -d10s -s random_id.lua http://localhost:8081

* t4: wrk 가 내부적으로 사용하는 스레드 갯수 (보통 cpu 코어 수)

* c100 : 서버와 동시에 유지하는 TCP 커넥션 수, (동접자 100명)

* d10s : 테스트를 얼마나 오래할건지. ==> 10초동안 테스트 할것이다.

 

*** 요청이 안간다면 application.properties 에서  server.address가 0.0.0.0 으로 수정해준다.

또한 시스템 방화벽 (윈도우 방화벽 & 리눅스 방화벽) 에서 인바운드 포트를 허용해주야 한다.

 

근데 초반에 1~2번 요청에서는 오히려 DB 쪽이 빠른 결과가 나올 수도 있다.

(캐시미스가 난다면 오히려 일반 DB조회가 더 빠를 수도 있음)

하지만 계속 요청을 보내 캐시를 쌓다 보면 점점 레디스 캐시쪽의 성능이 더 좋아지는 것을 알 수 있다.

 

- 3~4번 호출후 결과

 

구분/posts/cache (random_id.lua)/posts/nocache (random_id_nocache.lua)

구분 레디스_캐시 일반DB
Avg Latency (평균 응답 시간) 4.13 ms 9.45 ms
Max Latency  17.87 ms 68.20 ms
Latency Stdev 1.08 ms 4.79 ms
Requests/sec 23,857 rps 10,712 rps
총 요청 수 238,716 108,197
Transfer/sec 2.83 MB/s 1.27 MB/s

 

* Avg Latency (평균 응답 시간) 

* Max Latency  (최대 응답 시간) : 가장 느렸던 한 건의 요청 , tail latency 라고도 함.

* Latency Stdev (표준편차) : 응답 시간의 분포 정도.. 얼마나 일관되게 빠른지 측정하는 지표, 낮을수록 안정적

* Requests/sec (전체 처리량) : 서버가 1초에 처리한 전체 요청 수 , 핵심지표이며 높을수록 좋음.

* Req/Sec (Thread 기준 처리량) : wrk의 각 thread가 초당 처리한 요청 수

* +/- Stdev (%) : 전체 요청 중 평균 ± 표준편차 범위 안에 들어온 비율

* Transfer/sec : 초당 전송된 응답데이터양

 

 

 238716 requests in 10.01s, 28.27MB read : 

10.01초 동안 총 238,716개의 요청을 처리했고,
그 결과로 서버에서 28.27MB의 응답 데이터를 받았다

 

master 노드에서 replica 노드로 부하를 분산하게 된다고

하나의 요청에 대해서 처리량이 확 늘거나 초당 처리량이 눈에 띄게 증가하지 않는다.

확장성과 마스터노드에 대해서 부하가 줄어드는지와 읽기 요청을 얼마나 고루 분산하여 안정성을 확보하는게 중요하다.

 

 

1. Redis에 테스트 키 1개 삽입

redis-cli -p 6379 set test_key "hello_redis"

 

2. Master-Replica 구조 테스트용 설정파일

@Configuration
@EnableCaching
public class RedisConfig {

    //RedisConnectionFactory : Spring Data Redis에서 Redis 서버와의 연결을 생성하고 관리하는 인터페이스. 어떻게 연결하고 연결풀을 어떻게 유지할지를 정의함.
    //LettuceConnectionFactory : 구현체중 하나인데 기본 선택값임. 비동기/논블로킹 방식.
//    @Bean
//    public RedisConnectionFactory redisConnectionFactory(){
//        return new LettuceConnectionFactory();
//    }

    //센티넬을 통한 master-slave 노드 분산 기능 테스트
    @Bean
    @Primary
    public RedisConnectionFactory redisConnectionFactoryMasterOnly(){
        RedisSentinelConfiguration sentinelConfiguration = new RedisSentinelConfiguration()
                .master("mymaster")
                .sentinel("127.0.0.1", 26379);

        LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()
                .readFrom(ReadFrom.MASTER_PREFERRED) // 마스터에서 읽기 우선(부하 분산 테스트 하려고 이렇게 설정)

                .commandTimeout(Duration.ofSeconds(5))
                .build();
        return new LettuceConnectionFactory(sentinelConfiguration, clientConfig);
    }

    //부하 분산을 위한 테스트 replication 용
    @Bean
    public RedisConnectionFactory redisConnectionFactoryReplicaPreferred(){
        RedisSentinelConfiguration sentinelConfiguration =
                new RedisSentinelConfiguration().master("mymaster").sentinel("127.0.0.1", 26379);

        LettuceClientConfiguration clientConfiguration =
                LettucePoolingClientConfiguration.builder()
                        .poolConfig(new GenericObjectPoolConfig<>() {{
                            setMaxTotal(50);
                            setMaxIdle(50);
                            setMinIdle(10);
                        }})
                        .readFrom(ReadFrom.REPLICA_PREFERRED)
                        .commandTimeout(Duration.ofSeconds(5))
                        .build();
        return new LettuceConnectionFactory(sentinelConfiguration, clientConfiguration);

    }

    //캐시는 읽기전용이 아님 , 값이 없다면 DB에 값을 쓰거나 조건에 따라 키를 삭제한다.
    //Spring Cache는 GEt는 레플리카, Set은 마스터 이렇게 분기가 안되기 때문에 마스터로 고정한다.
    //또한 replica는 지연복제이기 때문에 여러대라면 각각의 상태가 다를 수 있다.
    @Bean
    public RedisCacheManager cacheManager(@Qualifier("redisConnectionFactoryMasterOnly") RedisConnectionFactory redisConnectionFactory){

                RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(10))

                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
                        new Jackson2JsonRedisSerializer<>(Object.class))) // 값 직렬화 방식 = Jackson을 사용한 JSON 직렬화
                .disableCachingNullValues(); // null 값은 캐싱하지 않도록 설정

                return RedisCacheManager.builder(redisConnectionFactory) // RedisConnectionFactory 기반으로 캐시 매니저 생성
                        .cacheDefaults(cacheConfig)
                        .build();
    }

//    @Bean
//    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory connectionFactory){
//
//        RedisTemplate<String, String> template = new RedisTemplate<>();
//        template.setConnectionFactory(connectionFactory);
//
//        //모든 키와 값이 UTF-8 문자열로 직렬화 된다.
//        template.setKeySerializer(new StringRedisSerializer());
//        template.setKeySerializer(new StringRedisSerializer());
//        template.setValueSerializer(new StringRedisSerializer());
//        template.setHashKeySerializer(new StringRedisSerializer());
//        template.setHashValueSerializer(new StringRedisSerializer());
//        return template;
//    }
//
//    @Bean
//    public RedisTemplate<String, Object> redisTemplateObject(RedisConnectionFactory redisConnectionFactory) {
//        RedisTemplate<String, Object> template = new RedisTemplate<>();
//        template.setConnectionFactory(redisConnectionFactory); // Redis 연결 팩토리 설정 (Redis와 연결)
//
//        // Key와 HashKey(해시의 키)는 일반 문자열로 직렬화
//        template.setKeySerializer(new StringRedisSerializer());
//        template.setHashKeySerializer(new StringRedisSerializer());
//
//        // Value와 HashValue(해시의 값)은 JSON 형식으로 직렬화
//        template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
//        template.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
//
//        template.afterPropertiesSet(); // Bean 설정 완료 후 초기화
//        return template;
//    }

    //master 노드만 읽기
    @Bean
    @Primary
    public RedisTemplate<String, String> redisTemplateMaster(
        @Qualifier("redisConnectionFactoryMasterOnly") RedisConnectionFactory cf
    ){
        RedisTemplate<String, String> template = new RedisTemplate<>();
        template.setConnectionFactory(cf);
        template.afterPropertiesSet();
        return template;
    }

    //replica 노드도 읽기
    @Bean
    public RedisTemplate<String, String> redisTemplateReplica(
            @Qualifier("redisConnectionFactoryReplicaPreferred")
            RedisConnectionFactory cf) {

        RedisTemplate<String, String> template = new RedisTemplate<>();
        template.setConnectionFactory(cf);
        template.afterPropertiesSet();
        return template;
    }

    //RedissonClient는 singleServer로 마스터에 직결될수 있음. , 얘 있어야 센티넬과 분리되서 실행가능함. 얘 주석하려면 센티넬 2대 이상 운용해야함
    @Bean
    public RedissonClient redissonClient(){
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://127.0.0.1:6379");
        return Redisson.create(config);
    }
}

 

 

3. 테스트용 api

@RestController
public class RedisReadTestController {

    private final RedisTemplate<String, String> redisTemplateMaster;
    private final RedisTemplate<String, String> redisTemplateReplica;

    public RedisReadTestController(
            @Qualifier("redisTemplateMaster") RedisTemplate<String, String> redisTemplateMaster,
            @Qualifier("redisTemplateReplica") RedisTemplate<String, String> redisTemplateReplica
    ) {
        this.redisTemplateMaster = redisTemplateMaster;
        this.redisTemplateReplica = redisTemplateReplica;
    }

    @GetMapping("/test/read/master")
    public String readMaster() {
        return redisTemplateMaster.opsForValue().get("test:key");
    }

    @GetMapping("/test/read/replica")
    public String readReplica() {
        return redisTemplateReplica.opsForValue().get("test:key");
    }
}

 

 

4. 테스트 호출

wrk -t4 -c100 -d10s http://localhost:8081/test/read/master

wrk -t4 -c100 -d10s http://localhost:8081/test/read/replica 로 호출을 하기전 

 

4-1 .키를 미리 하나 셋팅해놓는다 ==> redis-cli -p 6379 set test:key hello

4-2. 각 상태를 보기위해 cmd 3개로 monitor을 가동한다.

- redis-cli -p 6379 , redis-cli -p 6380, redis-cli -p 6381  /   monitor 

- master를 호출한다면 마스터 cmd에만 GET이 들어오는게 보이고 replica라면 레플리카에만 GET이 들어오는게 보일것이다.

*** JVM 인스턴스가 하나라면 replica중에서 하나에만 GET 요청이 올 것이다. 

 

Lettuce는 replica를 “connection마다” 선택하지 않는다.
“읽기 대상 노드(Read target)”를 한 번 정하고
그 이후 생성되는 모든 connection은 그 노드에 붙는다.

 

wsl을 2개띄어놓고 JVM도 포트를 달리하여 멀티로 가동후에 테스트하면 두개의 레플리카에서도 GET요청을 확인할 수 있다.

 

 

'DB > redis' 카테고리의 다른 글

[발표용] Redis  (0) 2026.04.02
[Redis] 분산락  (0) 2025.12.15
[Redis] Pub/Sub 실습  (0) 2025.06.12
Redis  (0) 2025.03.14

+ Recent posts