Redisレプリケーション構成と読み書き分離の実装ガイド

レプリケーション環境の構築

Dockerを使用してRedisのマスター・スレーブ構成を構築します。以下、Redis 6.0.3イメージを使用します。

docker pull redis:6.0.3

マスターノード設定

IP: 192.168.0.100、ポート: 6378

bind 0.0.0.0
port 6378
requirepass mySecretPass
daemonize yes
appendonly no
save ""
protected-mode no
repl-diskless-sync no

スレーブノード1の設定

IP: 192.168.0.100、ポート: 6377

bind 0.0.0.0
port 6377
requirepass mySecretPass
daemonize yes
appendonly no
save ""
protected-mode no
masterauth mySecretPass
replicaof 192.168.0.100 6378
replica-read-only yes

スレーブノード2の設定

IP: 192.168.0.100、ポート: 6376

bind 0.0.0.0
port 6376
requirepass mySecretPass
daemonize yes
appendonly no
save ""
protected-mode no
masterauth mySecretPass
replicaof 192.168.0.100 6378
replica-read-only yes

コンテナの起動

docker run -d --restart=always -m=1g --name redis-primary -p 6378:6378 --privileged=true -v /data/redis/conf/primary.conf:/usr/local/redis.conf -v /data/redis/data/primary:/data docker.io/redis:6.0.3 redis-server /usr/local/redis.conf

docker run -d --restart=always -m=1g --name redis-replica-1 -p 6377:6377 --privileged=true -v /data/redis/conf/replica1.conf:/usr/local/redis.conf -v /data/redis/data/replica1:/data docker.io/redis:6.0.3 redis-server /usr/local/redis.conf

docker run -d --restart=always -m=1g --name redis-replica-2 -p 6376:6376 --privileged=true -v /data/redis/conf/replica2.conf:/usr/local/redis.conf -v /data/redis/data/replica2:/data docker.io/redis:6.0.3 redis-server /usr/local/redis.conf

レプリケーション状態の確認

マスターノードでの確認:

192.168.0.100:6378> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=192.168.0.100,port=6377,state=online,offset=1063,lag=0
slave1:ip=192.168.0.100,port=6376,state=online,offset=1063,lag=1
master_replid:5f7d94600462d4861fafef71fd824cdeca59dc25
master_repl_offset:1063

スレーブノードでの確認:

192.168.0.100:6377> info replication
# Replication
role:slave
master_host:192.168.0.100
master_port:6378
master_link_status:up
slave_read_only:1

ファイアウォール設定

同期エラーが発生した場合は、ポートを開放してください。

firewall-cmd --zone=public --permanent --add-port=6376/tcp
firewall-cmd --zone=public --permanent --add-port=6377/tcp
firewall-cmd --zone=public --permanent --add-port=6378/tcp
firewall-cmd --reload

Spring Bootによる読み書き分離の実装

YAML設定

spring:
  redis:
    database: 5
    password: mySecretPass
    host: 192.168.0.100
    port: 6378
    replica-nodes:
      - host: 192.168.0.100
        port: 6377
      - host: 192.168.0.100
        port: 6376
    timeout: 60000ms
    lettuce:
      pool:
        max-active: 100
        max-idle: 20
        max-wait: 3000ms

カスタム条件アノテーション

import org.springframework.context.annotation.Conditional;
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@Conditional(ReplicaListCondition.class)
public @interface ConditionalOnReplicaList {
    String configKey() default "";
    String[] requiredFields() default {};
}

条件判定クラス

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.core.env.Environment;
import java.util.Objects;

@Slf4j
public class ReplicaListCondition extends SpringBootCondition {
    @Override
    public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
        var attrs = metadata.getAnnotationAttributes(ConditionalOnReplicaList.class.getName());
        String configKey = (String) attrs.get("configKey");
        String[] requiredFields = (String[]) attrs.get("requiredFields");
        
        Environment env = context.getEnvironment();
        boolean configured = false;
        
        for (String field : requiredFields) {
            String propKey = configKey + "[0]." + field;
            if (env.getProperty(propKey) != null) {
                configured = true;
                break;
            }
        }
        
        if (configured) {
            log.info("Redis レプリケーション構成で読み書き分離を有効化");
            return new ConditionOutcome(true, "Read-write separation enabled");
        }
        
        log.info("Redis 単一ノード構成で起動");
        return new ConditionOutcome(false, "Single node mode");
    }
}

Lettuce接続ファクトリの設定

@Bean
@ConditionalOnReplicaList(configKey = "spring.redis.replica-nodes", requiredFields = {"host", "port"})
public LettuceConnectionFactory redisConnectionFactory(RedisProperties props) {
    var replicaConfig = new RedisStaticMasterReplicaConfiguration(props.getHost(), props.getPort());
    replicaConfig.setDatabase(props.getDatabase());
    replicaConfig.setPassword(props.getPassword());
    
    for (RedisNode node : props.getReplicaNodes()) {
        replicaConfig.addNode(node.getHost(), node.getPort());
    }
    
    var readStrategy = new ReplicaPreferredReadStrategy();
    var clientConfig = LettuceClientConfiguration.builder()
        .readFrom(readStrategy)
        .build();
    
    return new LettuceConnectionFactory(replicaConfig, clientConfig);
}

読み取り戦略の実装

import io.lettuce.core.ReadFrom;
import io.lettuce.core.models.role.RedisInstance;
import io.lettuce.core.models.role.RedisNodeDescription;
import java.util.*;

public class ReplicaPreferredReadStrategy extends ReadFrom {
    private final Random random = new Random();
    
    @Override
    public List<RedisNodeDescription> select(Nodes nodes) {
        List<RedisNodeDescription> primaries = new ArrayList<>();
        List<RedisNodeDescription> replicas = new ArrayList<>();
        
        for (RedisNodeDescription node : nodes) {
            if (node.getRole() == RedisInstance.Role.MASTER) {
                primaries.add(node);
            } else if (node.getRole() == RedisInstance.Role.SLAVE) {
                replicas.add(node);
            }
        }
        
        if (!replicas.isEmpty()) {
            int selected = random.nextInt(replicas.size());
            return Collections.singletonList(replicas.get(selected));
        }
        
        if (!primaries.isEmpty()) {
            return Collections.singletonList(primaries.get(0));
        }
        
        return Collections.emptyList();
    }
}

設定プロパティクラス

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

@Data
@Component
@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {
    private int database;
    private String password;
    private String host;
    private int port;
    private List<RedisNode> replicaNodes;
    
    @Data
    public static class RedisNode {
        private String host;
        private int port;
    }
}

タグ: redis Docker Spring Boot Lettuce レプリケーション

7月1日 17:00 投稿