관리 메뉴

ballqs 님의 블로그

[Spring] Redis Cluster localhost에 구현하여 적용 본문

코딩 공부/Spring

[Spring] Redis Cluster localhost에 구현하여 적용

ballqs 2024. 10. 15. 22:10

출처 : https://redis.io/redis-enterprise/technology/linear-scaling-redis-enterprise/

 

Redis Cluster 란?

데이터의 고가용성과 분산 처리를 위해 Redis가 제공하는 기능이다.

이를 통해 여러 노드에 데이터를 분산 저장하고, 장애 발생 시 자동으로 복구하거나 재구성할 수 있는 클러스터 환경을 제공한다.

 

Redis Cluster 특징

데이터 샤딩 (Sharding)

Redis Cluster는 데이터를 여러 노드에 분산해서 저정하는 방식이다. 전체 키 공간을 16,384개의 슬롯(Slot)으로 나누고, 각 노드가 이 슬롯 중 일부를 담당한다. 키는 해시 함수에 의해 슬롯 중 하나에 매핑되고, 해당 슬롯을 가진 노드에 저장된다. 이를 통해 여러 노드에 데이터를 분산할 수 있다.

 

고가용성 (High Availability)

Redis Cluster는 자동으로 장애 조치를 할 수 있다. 노드가 장애를 일으키면, 클러스터 내에 있는 다른 복제 노드(replica)가 자동으로 승격되며 마스터 노드로 전환된다. 이를 통해 서비스 중단 없이 데이터를 사용할 수 있게 한다.

 

복제 (Replication)

Redis Cluster는 각 마스터 노드마다 하나 이상의 슬레이브 노드(복제본 노드)를 가질 수 있다. 이 복제본 노드는 마스터 노드가 장애가 발생했을 때 자동으로 마스터로 전환될 준비를 합니다. 복제는 데이터 일관성을 높이고 데이터 손실을 방지하는 데 도움이 됩니다.

 

노드 간 통신 (Gossip Protocol)

Redis Cluster의 노드들은 서로 상태 정보를 교환하기 위해 Gossip 프로토콜을 사용한다. 이를 통해 클러스터 내에서 노드의 상태를 확인하고, 장애가 발생한 노드를 감지하거나 클러스터를 재구성할 수 있다.

 

파티셔닝과 확장성 (Partitioning & Scalability)

Redis Cluster는 데이터를 여러 노드에 분산시켜 처리하므로, 노드 수를 늘리면 저장 용량과 처리 능력을 확장할 수 있다. 이를 통해 고성능을 요구하는 대규모 시스템에서도 효율적으로 운영할 수 있다.

 

클라이언트 측 리다이렉션

Redis Cluster는 데이터를 분산 처리하기 때문에, 클라이언트가 요청을 잘못된 노드에 보낼 수 있다. 이때 클라이언트는 MOVED라는 리다이렉션 메시지를 받아서 해당 데이터를 저장하고 있는 올바른 노드로 요청을 다시 보낼 수 있다.

 


Localhost에서 진행하기

※Window의 경우 우선적으로 WSL가 깔려 있지 않으면 따라하기 어렵다. 또한 redis는 깔려 있다는 전제하에 작성한다.

 

참조사이트 : 링크

 

port 별 폴더 생성

mkdir -p {7000..7005}

 

redis.conf 파일을 port별로 작성

# Redis 인스턴스가 사용할 포트를 설정합니다.
port [port]

# 클러스터 모드를 활성화합니다.
cluster-enabled yes

# 클러스터 노드 정보를 저장할 파일을 지정합니다.
cluster-config-file nodes-[port].conf

# 노드의 타임아웃 시간을 설정합니다.
cluster-node-timeout 5000

# 데이터의 영속성을 위해 AOF(Append Only File) 모드를 활성화합니다.
appendonly yes

 

 

redis.conf 실행

redis-server ./[port]/redis.conf

 

실행 화면

 

6개의 port에 있는 모든 redis.conf을 실행시켜 준다.

 

Cluster 생성

redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 
127.0.0.1:7004 127.0.0.1:7005 --cluster-replicas 1

이 명령어는 6개의 Redis 노드를 클러스터로 생성하며, 각 마스터 노드에 대해 1개의 슬레이브 노드를 설정합니다. 이 명령어를 실행하면 Redis Cluster가 자동으로 구성됩니다.

 

Cluster Nodes 확인

redis-cli -p [port] cluster nodes

 

Nodes 죽여보기

pkill -f "redis-server.*[port]"

 

7000 포트

 

7003 포트 슬레이브 -> 마스터 승격

 

결과 확인

 

7000 포트를 죽이니까 7003 가 슬레이브에서 마스터로 승격한 것을 확인할수있다.

 

Spring boot 적용

application.yml 설정

spring:
  data:
    redis:
      cluster:
        nodes: 127.0.0.1:7000 , 127.0.0.1:7001 , 127.0.0.1:7002 , 127.0.0.1:7003 , 127.0.0.1:7004 , 127.0.0.1:7005

 

RedisConfig.java 작성

@Slf4j
@EnableRedisRepositories
@RequiredArgsConstructor
@Configuration
public class RedisConfig {

    @Value("${spring.data.redis.cluster.nodes}")
    private List<String> nodes;

    @Bean
    public RedisMessageListenerContainer redisMessageListener(
            RedisConnectionFactory connectionFactory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);

        return container;
    }

    @Primary
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setEnableTransactionSupport(true);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        List<RedisNode> redisNodes = nodes.stream()
                .map(node -> {
                    String[] parts = node.split(":");
                    return new RedisNode(parts[0], Integer.parseInt(parts[1]));
                }).toList();
        RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration();
        redisClusterConfiguration.setClusterNodes(redisNodes);
        return new LettuceConnectionFactory(redisClusterConfiguration);
    }

    @Bean
    public RedissonClient redissonClient() {
        final Config config = new Config();

        ClusterServersConfig csc = config.useClusterServers()
                .setScanInterval(2000)
                .setConnectTimeout(100)
                .setTimeout(3000)
                .setRetryAttempts(3)
                .setRetryInterval(1500);

        nodes.forEach(node -> csc.addNodeAddress("redis://" + node));

        return Redisson.create(config);
    }
}

 

RedisController.java 작성

@RestController
@RequiredArgsConstructor
@RequestMapping("/redis")
@Slf4j
public class RedisController {

    private final RedisService redisService;

    @PostMapping
    public void create(@RequestBody CreateDto createDto) {
        redisService.create(createDto);
    }

    @GetMapping("/{id}")
    public void select(@PathVariable String id) {
        redisService.select(id);
    }
}

 

RedisService.java 작성

@Slf4j(topic = "StockRedisService")
@RequiredArgsConstructor
@Service
public class RedisService {

    private final RedisTemplate<String , Object> redisTemplate;

    private final String KEY_NAME = "stock";

    public void create(CreateDto createDto) {
        HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash();

        String key = KEY_NAME + ":" + createDto.getId();

        hashOperations.put(key, "id", createDto.getId());
        hashOperations.put(key, "name", createDto.getName());
        hashOperations.put(key, "cnt", createDto.getCnt());
        hashOperations.getOperations().expire(key, 600, TimeUnit.SECONDS);
    }

    public void select(String id) {
        HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash();
        String key = KEY_NAME + ":" + id;

        Map<String, Object> entries = hashOperations.entries(key);
        if (!entries.isEmpty()) {
            log.info("Selected stock id: {}", entries.get("id"));
            log.info("Selected stock name: {}", entries.get("name"));
            log.info("Selected stock cnt: {}", entries.get("cnt"));
        }
    }
}

 

insert 결과물

 

Select 결과물