diff --git a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/LettuceConnectionConfiguration.java b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/LettuceConnectionConfiguration.java index b840cd6db3e4..7d4d6fc2e044 100644 --- a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/LettuceConnectionConfiguration.java +++ b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/LettuceConnectionConfiguration.java @@ -17,6 +17,8 @@ package org.springframework.boot.data.redis.autoconfigure; import java.time.Duration; +import java.util.Collections; +import java.util.List; import io.lettuce.core.ClientOptions; import io.lettuce.core.ReadFrom; @@ -37,7 +39,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading; +import org.springframework.boot.data.redis.autoconfigure.RedisConnectionDetails.Node; import org.springframework.boot.data.redis.autoconfigure.RedisProperties.Lettuce.Cluster.Refresh; +import org.springframework.boot.data.redis.autoconfigure.RedisProperties.Lettuce.StaticMasterReplica; import org.springframework.boot.data.redis.autoconfigure.RedisProperties.Pool; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslOptions; @@ -49,11 +53,13 @@ import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.RedisStaticMasterReplicaConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceClientConfigurationBuilder; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** @@ -64,6 +70,7 @@ * @author Moritz Halbritter * @author Phillip Webb * @author Scott Frederick + * @author Yong-Hyun Kim */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(RedisClient.class) @@ -120,6 +127,12 @@ private LettuceConnectionFactory createConnectionFactory( LettuceClientConfiguration clientConfiguration = getLettuceClientConfiguration( clientConfigurationBuilderCustomizers, clientOptionsBuilderCustomizers, clientResources, getProperties().getLettuce().getPool()); + + RedisStaticMasterReplicaConfiguration staticMasterReplicaConfiguration = getStaticMasterReplicaConfiguration(); + if (staticMasterReplicaConfiguration != null) { + return new LettuceConnectionFactory(staticMasterReplicaConfiguration, clientConfiguration); + } + return switch (this.mode) { case STANDALONE -> new LettuceConnectionFactory(getStandaloneConfig(), clientConfiguration); case CLUSTER -> { @@ -135,6 +148,26 @@ private LettuceConnectionFactory createConnectionFactory( }; } + private @Nullable RedisStaticMasterReplicaConfiguration getStaticMasterReplicaConfiguration() { + StaticMasterReplica staticMasterReplica = getProperties().getLettuce().getStaticMasterReplica(); + + if (!CollectionUtils.isEmpty(staticMasterReplica.getNodes())) { + List nodes = asNodes(staticMasterReplica.getNodes()); + RedisStaticMasterReplicaConfiguration configuration = new RedisStaticMasterReplicaConfiguration( + nodes.get(0).host(), nodes.get(0).port()); + configuration.setUsername(getProperties().getUsername()); + if (StringUtils.hasText(getProperties().getPassword())) { + configuration.setPassword(getProperties().getPassword()); + } + configuration.setDatabase(getProperties().getDatabase()); + nodes.stream().skip(1).forEach((node) -> configuration.addNode(node.host(), node.port())); + + return configuration; + } + + return null; + } + private LettuceClientConfiguration getLettuceClientConfiguration( ObjectProvider clientConfigurationBuilderCustomizers, ObjectProvider clientOptionsBuilderCustomizers, @@ -250,6 +283,20 @@ private void customizeConfigurationFromUrl(LettuceClientConfiguration.LettuceCli } } + private List asNodes(@Nullable List nodes) { + if (nodes == null) { + return Collections.emptyList(); + } + return nodes.stream().map(this::asNode).toList(); + } + + private Node asNode(String node) { + int portSeparatorIndex = node.lastIndexOf(':'); + String host = node.substring(0, portSeparatorIndex); + int port = Integer.parseInt(node.substring(portSeparatorIndex + 1)); + return new Node(host, port); + } + /** * Inner class to allow optional commons-pool2 dependency. */ diff --git a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisProperties.java b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisProperties.java index 344a3387850f..f2024da21d6d 100644 --- a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisProperties.java +++ b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisProperties.java @@ -34,6 +34,7 @@ * @author Stephane Nicoll * @author Scott Frederick * @author Yanming Zhou + * @author Yong-Hyun Kim * @since 4.0.0 */ @ConfigurationProperties("spring.data.redis") @@ -482,6 +483,8 @@ public static class Lettuce { private final Cluster cluster = new Cluster(); + private final StaticMasterReplica staticMasterReplica = new StaticMasterReplica(); + public Duration getShutdownTimeout() { return this.shutdownTimeout; } @@ -506,6 +509,10 @@ public Cluster getCluster() { return this.cluster; } + public StaticMasterReplica getStaticMasterReplica() { + return this.staticMasterReplica; + } + public static class Cluster { private final Refresh refresh = new Refresh(); @@ -562,6 +569,27 @@ public void setAdaptive(boolean adaptive) { } + /** + * Lettuce static master-replica properties. + */ + public static class StaticMasterReplica { + + /** + * List of "host:port" pairs regardless of role as the actual roles are + * determined by querying each node's ROLE command. + */ + private @Nullable List nodes; + + public @Nullable List getNodes() { + return this.nodes; + } + + public void setNodes(@Nullable List nodes) { + this.nodes = nodes; + } + + } + } } diff --git a/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/RedisAutoConfigurationTests.java b/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/RedisAutoConfigurationTests.java index fc1b8dba6f55..e974ad2259db 100644 --- a/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/RedisAutoConfigurationTests.java +++ b/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/RedisAutoConfigurationTests.java @@ -91,6 +91,7 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Yong-Hyun Kim */ class RedisAutoConfigurationTests { @@ -501,6 +502,38 @@ void testRedisConfigurationWithClusterAndAuthentication() { ); } + @Test + void testRedisConfigurationWithStaticMasterReplica() { + List staticMasterReplicaNodes = Arrays.asList("127.0.0.1:28319", "127.0.0.1:28320", "[::1]:28321"); + this.contextRunner + .withPropertyValues( + "spring.data.redis.lettuce.static-master-replica.nodes[0]:" + staticMasterReplicaNodes.get(0), + "spring.data.redis.lettuce.static-master-replica.nodes[1]:" + staticMasterReplicaNodes.get(1), + "spring.data.redis.lettuce.static-master-replica.nodes[2]:" + staticMasterReplicaNodes.get(2)) + .run((context) -> { + LettuceConnectionFactory connectionFactory = context.getBean(LettuceConnectionFactory.class); + assertThat(connectionFactory.getSentinelConfiguration()).isNull(); + assertThat(connectionFactory.getClusterConfiguration()).isNull(); + assertThat(isStaticMasterReplicaAware(connectionFactory)).isTrue(); + }); + } + + @Test + void testRedisConfigurationWithStaticMasterReplicaAndAuthenticationAndDatabase() { + List staticMasterReplicaNodes = Arrays.asList("127.0.0.1:28319", "127.0.0.1:28320"); + this.contextRunner + .withPropertyValues("spring.data.redis.username=user", "spring.data.redis.password=password", + "spring.data.redis.database=1", + "spring.data.redis.lettuce.static-master-replica.nodes[0]:" + staticMasterReplicaNodes.get(0), + "spring.data.redis.lettuce.static-master-replica.nodes[1]:" + staticMasterReplicaNodes.get(1)) + .run((context) -> { + LettuceConnectionFactory connectionFactory = context.getBean(LettuceConnectionFactory.class); + assertThat(getUserName(connectionFactory)).isEqualTo("user"); + assertThat(connectionFactory.getPassword()).isEqualTo("password"); + assertThat(connectionFactory.getDatabase()).isOne(); + }); + } + @Test void testRedisConfigurationCreateClientOptionsByDefault() { this.contextRunner.run(assertClientOptions(ClientOptions.class, (options) -> { @@ -704,6 +737,10 @@ private RedisClusterNode createRedisNode(String host) { return node; } + private boolean isStaticMasterReplicaAware(LettuceConnectionFactory factory) { + return ReflectionTestUtils.invokeMethod(factory, "isStaticMasterReplicaAware"); + } + private static final class RedisNodes implements Nodes { private final List descriptions;