レプリケーション環境の構築
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;
}
}