From 5082eee03f0cd51c993338e63be823a4825a01a2 Mon Sep 17 00:00:00 2001 From: facewise Date: Tue, 26 Aug 2025 10:55:19 +0900 Subject: [PATCH 1/3] Support Redis static master-replica auto-configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 용현 --- .../JedisConnectionConfiguration.java | 13 +++-- .../LettuceConnectionConfiguration.java | 11 +++- .../PropertiesRedisConnectionDetails.java | 38 +++++++++++++ .../RedisConnectionConfiguration.java | 40 ++++++++++++- .../autoconfigure/RedisConnectionDetails.java | 56 ++++++++++++++++--- .../redis/autoconfigure/RedisProperties.java | 32 +++++++++++ ...PropertiesRedisConnectionDetailsTests.java | 26 +++++++++ .../RedisAutoConfigurationJedisTests.java | 9 +++ .../RedisAutoConfigurationTests.java | 46 +++++++++++++++ 9 files changed, 257 insertions(+), 14 deletions(-) diff --git a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/JedisConnectionConfiguration.java b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/JedisConnectionConfiguration.java index 8ff0da512d5d..6624ada8d1cc 100644 --- a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/JedisConnectionConfiguration.java +++ b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/JedisConnectionConfiguration.java @@ -38,6 +38,7 @@ 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.jedis.JedisClientConfiguration; import org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisClientConfigurationBuilder; import org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisSslClientConfigurationBuilder; @@ -55,6 +56,7 @@ * @author Andy Wilkinson * @author Phillip Webb * @author Scott Frederick + * @author Yong-Hyun Kim */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ GenericObjectPool.class, JedisConnection.class, Jedis.class }) @@ -64,10 +66,12 @@ class JedisConnectionConfiguration extends RedisConnectionConfiguration { JedisConnectionConfiguration(RedisProperties properties, ObjectProvider standaloneConfigurationProvider, - ObjectProvider sentinelConfiguration, - ObjectProvider clusterConfiguration, RedisConnectionDetails connectionDetails) { - super(properties, connectionDetails, standaloneConfigurationProvider, sentinelConfiguration, - clusterConfiguration); + ObjectProvider sentinelConfigurationProvider, + ObjectProvider clusterConfigurationProvider, + ObjectProvider staticMasterReplicaConfigurationProvider, + RedisConnectionDetails connectionDetails) { + super(properties, connectionDetails, standaloneConfigurationProvider, sentinelConfigurationProvider, + clusterConfigurationProvider, staticMasterReplicaConfigurationProvider); } @Bean @@ -103,6 +107,7 @@ private JedisConnectionFactory createJedisConnectionFactory( Assert.state(sentinelConfig != null, "'sentinelConfig' must not be null"); yield new JedisConnectionFactory(sentinelConfig, clientConfiguration); } + case STATIC_MASTER_REPLICA -> throw new IllegalStateException("Static master-replica mode is not supported by Jedis"); }; } 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..b9c2561235b2 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 @@ -49,6 +49,7 @@ 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; @@ -64,6 +65,7 @@ * @author Moritz Halbritter * @author Phillip Webb * @author Scott Frederick + * @author Yong-Hyun Kim */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(RedisClient.class) @@ -74,9 +76,10 @@ class LettuceConnectionConfiguration extends RedisConnectionConfiguration { ObjectProvider standaloneConfigurationProvider, ObjectProvider sentinelConfigurationProvider, ObjectProvider clusterConfigurationProvider, + ObjectProvider staticMasterReplicaConfigurationProvider, RedisConnectionDetails connectionDetails) { super(properties, connectionDetails, standaloneConfigurationProvider, sentinelConfigurationProvider, - clusterConfigurationProvider); + clusterConfigurationProvider, staticMasterReplicaConfigurationProvider); } @Bean(destroyMethod = "shutdown") @@ -132,6 +135,12 @@ private LettuceConnectionFactory createConnectionFactory( Assert.state(sentinelConfig != null, "'sentinelConfig' must not be null"); yield new LettuceConnectionFactory(sentinelConfig, clientConfiguration); } + case STATIC_MASTER_REPLICA -> { + RedisStaticMasterReplicaConfiguration staticMasterReplicaConfiguration = getStaticMasterReplicaConfiguration(); + Assert.state(staticMasterReplicaConfiguration != null, + "'staticMasterReplicaConfiguration' must not be null"); + yield new LettuceConnectionFactory(staticMasterReplicaConfiguration, clientConfiguration); + } }; } diff --git a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/PropertiesRedisConnectionDetails.java b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/PropertiesRedisConnectionDetails.java index b1328a42492b..6558f6b1242a 100644 --- a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/PropertiesRedisConnectionDetails.java +++ b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/PropertiesRedisConnectionDetails.java @@ -35,6 +35,7 @@ * @author Scott Frederick * @author Yanming Zhou * @author Phillip Webb + * @author Yong-Hyun Kim */ class PropertiesRedisConnectionDetails implements RedisConnectionDetails { @@ -92,6 +93,12 @@ public Standalone getStandalone() { return (cluster != null) ? new PropertiesCluster(cluster) : null; } + @Override + public @Nullable StaticMasterReplica getStaticMasterReplica() { + RedisProperties.StaticMasterReplica staticMasterReplica = this.properties.getStaticMasterReplica(); + return (staticMasterReplica != null) ? new PropertiesStaticMasterReplica(getStandalone().getDatabase(), staticMasterReplica) : null; + } + private @Nullable RedisUrl getRedisUrl() { return RedisUrl.of(this.properties.getUrl()); } @@ -181,4 +188,35 @@ public List getNodes() { } + /** + * {@link StaticMasterReplica} implementation backed by properties. + */ + private class PropertiesStaticMasterReplica implements StaticMasterReplica { + + private final int database; + + private final RedisProperties.StaticMasterReplica properties; + + PropertiesStaticMasterReplica(int database, RedisProperties.StaticMasterReplica properties) { + this.database = database; + this.properties = properties; + } + + @Override + public int getDatabase() { + return this.database; + } + + @Override + public List getNodes() { + return asNodes(this.properties.getNodes()); + } + + @Override + public @Nullable SslBundle getSslBundle() { + return PropertiesRedisConnectionDetails.this.getSslBundle(); + } + + } + } diff --git a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisConnectionConfiguration.java b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisConnectionConfiguration.java index 024d7519b249..faf2e51b17c3 100644 --- a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisConnectionConfiguration.java +++ b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisConnectionConfiguration.java @@ -33,6 +33,7 @@ import org.springframework.data.redis.connection.RedisPassword; import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.RedisStaticMasterReplicaConfiguration; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -48,6 +49,7 @@ * @author Andy Wilkinson * @author Phillip Webb * @author Yanming Zhou + * @author Yong-Hyun Kim */ abstract class RedisConnectionConfiguration { @@ -62,6 +64,8 @@ abstract class RedisConnectionConfiguration { private final @Nullable RedisClusterConfiguration clusterConfiguration; + private final @Nullable RedisStaticMasterReplicaConfiguration staticMasterReplicaConfiguration; + private final RedisConnectionDetails connectionDetails; protected final Mode mode; @@ -69,12 +73,14 @@ abstract class RedisConnectionConfiguration { protected RedisConnectionConfiguration(RedisProperties properties, RedisConnectionDetails connectionDetails, ObjectProvider standaloneConfigurationProvider, ObjectProvider sentinelConfigurationProvider, - ObjectProvider clusterConfigurationProvider) { + ObjectProvider clusterConfigurationProvider, + ObjectProvider staticMasterReplicaConfigurationProvider) { this.properties = properties; this.standaloneConfiguration = standaloneConfigurationProvider.getIfAvailable(); this.sentinelConfiguration = sentinelConfigurationProvider.getIfAvailable(); this.clusterConfiguration = clusterConfigurationProvider.getIfAvailable(); this.connectionDetails = connectionDetails; + this.staticMasterReplicaConfiguration = staticMasterReplicaConfigurationProvider.getIfAvailable(); this.mode = determineMode(); } @@ -142,6 +148,31 @@ protected final RedisStandaloneConfiguration getStandaloneConfig() { return null; } + /** + * Create a {@link RedisStaticMasterReplicaConfiguration} if necessary. + * @return {@literal null} if no static master-replica settings are set. + */ + protected final @Nullable RedisStaticMasterReplicaConfiguration getStaticMasterReplicaConfiguration() { + if (this.staticMasterReplicaConfiguration != null) { + return this.staticMasterReplicaConfiguration; + } + if (this.connectionDetails.getStaticMasterReplica() != null) { + List nodes = this.connectionDetails.getStaticMasterReplica().getNodes(); + Assert.notEmpty(nodes, "'staticMasterReplica.nodes' must not be empty'"); + Node firstNode = nodes.get(0); + RedisStaticMasterReplicaConfiguration config = new RedisStaticMasterReplicaConfiguration(firstNode.host(), firstNode.port()); + nodes.stream().skip(1).forEach(node -> config.addNode(node.host(), node.port())); + config.setUsername(this.connectionDetails.getUsername()); + String password = this.connectionDetails.getPassword(); + if (password != null) { + config.setPassword(RedisPassword.of(password)); + } + config.setDatabase(this.connectionDetails.getStaticMasterReplica().getDatabase()); + return config; + } + return null; + } + private List getNodes(Cluster cluster) { return cluster.getNodes().stream().map(this::asRedisNode).toList(); } @@ -162,6 +193,8 @@ protected final RedisProperties getProperties() { ? this.connectionDetails.getCluster().getSslBundle() : null; case SENTINEL -> (this.connectionDetails.getSentinel() != null) ? this.connectionDetails.getSentinel().getSslBundle() : null; + case STATIC_MASTER_REPLICA -> (this.connectionDetails.getStaticMasterReplica() != null) + ? this.connectionDetails.getStaticMasterReplica().getSslBundle() : null; }; } @@ -197,12 +230,15 @@ private Mode determineMode() { if (getClusterConfiguration() != null) { return Mode.CLUSTER; } + if (getStaticMasterReplicaConfiguration() != null) { + return Mode.STATIC_MASTER_REPLICA; + } return Mode.STANDALONE; } enum Mode { - STANDALONE, CLUSTER, SENTINEL + STANDALONE, CLUSTER, SENTINEL, STATIC_MASTER_REPLICA } diff --git a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisConnectionDetails.java b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisConnectionDetails.java index 2a72c78eea75..4f27a6c8d744 100644 --- a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisConnectionDetails.java +++ b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisConnectionDetails.java @@ -29,6 +29,7 @@ * * @author Moritz Halbritter * @author Andy Wilkinson + * @author Yong-Hyun Kim * @since 4.0.0 */ public interface RedisConnectionDetails extends ConnectionDetails { @@ -50,8 +51,8 @@ public interface RedisConnectionDetails extends ConnectionDetails { } /** - * Redis standalone configuration. Mutually exclusive with {@link #getSentinel()} and - * {@link #getCluster()}. + * Redis standalone configuration. Mutually exclusive with {@link #getSentinel()}, + * {@link #getCluster()} and {@link #getStaticMasterReplica()}. * @return the Redis standalone configuration */ default @Nullable Standalone getStandalone() { @@ -59,8 +60,8 @@ public interface RedisConnectionDetails extends ConnectionDetails { } /** - * Redis sentinel configuration. Mutually exclusive with {@link #getStandalone()} and - * {@link #getCluster()}. + * Redis sentinel configuration. Mutually exclusive with {@link #getStandalone()}, + * {@link #getCluster()} and {@link #getStaticMasterReplica()}. * @return the Redis sentinel configuration */ default @Nullable Sentinel getSentinel() { @@ -68,14 +69,23 @@ public interface RedisConnectionDetails extends ConnectionDetails { } /** - * Redis cluster configuration. Mutually exclusive with {@link #getStandalone()} and - * {@link #getSentinel()}. + * Redis cluster configuration. Mutually exclusive with {@link #getStandalone()}, + * {@link #getSentinel()} and {@link #getStaticMasterReplica()}. * @return the Redis cluster configuration */ default @Nullable Cluster getCluster() { return null; } + /** + * Redis static Master / Replica configuration. Mutually exclusive with {@link #getStandalone()}, + * {@link #getSentinel()} and {@link #getCluster()}. + * @return the Redis static Master / Replica configuration + */ + default @Nullable StaticMasterReplica getStaticMasterReplica() { + return null; + } + /** * Redis standalone configuration. */ @@ -245,7 +255,39 @@ interface Cluster { } /** - * A node in a sentinel or cluster configuration. + * Redis static Master / Replica configuration. + */ + interface StaticMasterReplica { + + /** + * Database index used by the connection factory. + * @return the database index used by the connection factory + */ + default int getDatabase() { + return 0; + } + + /** + * List of Master and Replica nodes for the static configuration. This represents + * the nodes to be used in a static Master/Replica setup and is required to have + * at least one entry. The first node does not need to be the master, as the actual + * roles are determined by querying each node's ROLE command. + * @return the list of nodes for Master/Replica configuration + */ + List getNodes(); + + /** + * SSL bundle to use. + * @return the SSL bundle to use + */ + default @Nullable SslBundle getSslBundle() { + return null; + } + + } + + /** + * A node in a sentinel, cluster or static master-replica configuration. * * @param host the hostname of the node * @param port the port of the node 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..5adf0c633654 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") @@ -94,6 +95,8 @@ public class RedisProperties { private @Nullable Cluster cluster; + private @Nullable StaticMasterReplica staticMasterReplica; + private final Ssl ssl = new Ssl(); private final Jedis jedis = new Jedis(); @@ -200,6 +203,14 @@ public void setCluster(@Nullable Cluster cluster) { this.cluster = cluster; } + public @Nullable StaticMasterReplica getStaticMasterReplica() { + return this.staticMasterReplica; + } + + public void setStaticMasterReplica(@Nullable StaticMasterReplica staticMasterReplica) { + this.staticMasterReplica = staticMasterReplica; + } + public Jedis getJedis() { return this.jedis; } @@ -413,6 +424,27 @@ public void setPassword(@Nullable String password) { } + /** + * Redis 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; + } + + } + public static class Ssl { /** diff --git a/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/PropertiesRedisConnectionDetailsTests.java b/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/PropertiesRedisConnectionDetailsTests.java index 9e87ca435e0f..53a800338668 100644 --- a/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/PropertiesRedisConnectionDetailsTests.java +++ b/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/PropertiesRedisConnectionDetailsTests.java @@ -33,6 +33,7 @@ * * @author Scott Frederick * @author Moritz Halbritter + * @author Yong-Hyun Kim */ class PropertiesRedisConnectionDetailsTests { @@ -160,6 +161,19 @@ void sentinelIsConfigured() { assertThat(connectionDetails.getSentinel().getDatabase()).isEqualTo(5); } + @Test + void staticMasterReplicaIsConfigured() { + RedisProperties.StaticMasterReplica staticMasterReplica = new RedisProperties.StaticMasterReplica(); + staticMasterReplica.setNodes(List.of("localhost:1111", "127.0.0.1:2222", "[::1]:3333")); + this.properties.setStaticMasterReplica(staticMasterReplica); + this.properties.setDatabase(5); + PropertiesRedisConnectionDetails connectionDetails = new PropertiesRedisConnectionDetails(this.properties, + null); + assertThat(connectionDetails.getStaticMasterReplica().getNodes()).containsExactly(new Node("localhost", 1111), + new Node("127.0.0.1", 2222), new Node("[::1]", 3333)); + assertThat(connectionDetails.getStaticMasterReplica().getDatabase()).isEqualTo(5); + } + @Test void sentinelDatabaseIsConfiguredFromUrl() { RedisProperties.Sentinel sentinel = new RedisProperties.Sentinel(); @@ -172,6 +186,18 @@ void sentinelDatabaseIsConfiguredFromUrl() { assertThat(connectionDetails.getSentinel().getDatabase()).isEqualTo(9999); } + @Test + void staticMasterReplicaDatabaseIsConfiguredFromUrl() { + RedisProperties.StaticMasterReplica staticMasterReplica = new RedisProperties.StaticMasterReplica(); + staticMasterReplica.setNodes(List.of("localhost:1111", "127.0.0.1:2222", "[::1]:3333")); + this.properties.setStaticMasterReplica(staticMasterReplica); + this.properties.setUrl("redis://example.com:1234/9999"); + this.properties.setDatabase(5); + PropertiesRedisConnectionDetails connectionDetails = new PropertiesRedisConnectionDetails(this.properties, + null); + assertThat(connectionDetails.getStaticMasterReplica().getDatabase()).isEqualTo(9999); + } + @Test void shouldReturnSslBundle() { SslBundle bundle1 = mock(SslBundle.class); diff --git a/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/RedisAutoConfigurationJedisTests.java b/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/RedisAutoConfigurationJedisTests.java index bbe6f7845f8a..cfc41012d671 100644 --- a/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/RedisAutoConfigurationJedisTests.java +++ b/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/RedisAutoConfigurationJedisTests.java @@ -49,6 +49,7 @@ * @author Andy Wilkinson * @author Phillip Webb * @author Scott Frederick + * @author Yong-Hyun Kim */ @ClassPathExclusions("lettuce-core-*.jar") class RedisAutoConfigurationJedisTests { @@ -247,6 +248,14 @@ void testRedisConfigurationWithCluster() { .isTrue()); } + @Test + void testRedisConfigurationWithStaticMasterReplica() { + this.contextRunner.withPropertyValues("spring.data.redis.static-master-replica.nodes=127.0.0.1:27379,127.0.0.1:27380") + .withUserConfiguration(JedisConnectionFactoryCaptorConfiguration.class) + .run((context) -> assertThat(JedisConnectionFactoryCaptor.connectionFactory.isRedisClusterAware()) + .isTrue()); + } + @Test void testRedisConfigurationWithSslEnabled() { this.contextRunner.withPropertyValues("spring.data.redis.ssl.enabled:true").run((context) -> { 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..f67d6390d8c0 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 @@ -61,6 +61,7 @@ import org.springframework.data.redis.connection.RedisPassword; 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.jedis.JedisConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceClientConfigurationBuilder; @@ -91,6 +92,7 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Yong-Hyun Kim */ class RedisAutoConfigurationTests { @@ -678,6 +680,50 @@ void shouldUseVirtualThreadsIfEnabled() { }); } + @Test + void testRedisConfigurationWithStaticMasterReplica() { + this.contextRunner + .withPropertyValues("spring.data.redis.static-master-replica.nodes=127.0.0.1:27379,127.0.0.1:27380") + .run((context) -> { + LettuceConnectionFactory connectionFactory = context.getBean(LettuceConnectionFactory.class); + Object configuration = ReflectionTestUtils.getField(connectionFactory, "configuration"); + assertThat(configuration) + .isInstanceOf(RedisStaticMasterReplicaConfiguration.class); + assertThat(connectionFactory.getSentinelConfiguration()).isNull(); + assertThat(connectionFactory.getClusterConfiguration()).isNull(); + }); + } + + @Test + void testRedisConfigurationWithStaticMasterReplicaAndDatabaseAndAuthentication() { + this.contextRunner + .withPropertyValues( + "spring.data.redis.static-master-replica.nodes:127.0.0.1:27379,127.0.0.1:27380", + "spring.data.redis.database:1", + "spring.data.redis.username:user", + "spring.data.redis.password:password") + .run((context) -> { + LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); + assertThat(cf.getDatabase()).isOne(); + assertThat(getUserName(cf)).isEqualTo("user"); + assertThat(cf.getPassword()).isEqualTo("password"); + }); + } + + @Test + void testRedisConfigurationWithStaticMasterReplicaDatabaseAndCredentialsFromUrl() { + this.contextRunner + .withPropertyValues( + "spring.data.redis.url:redis://user:password@localhost:6379/5", + "spring.data.redis.static-master-replica.nodes:127.0.0.1:27379,127.0.0.1:27380") + .run((context) -> { + LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); + assertThat(cf.getDatabase()).isEqualTo(5); + assertThat(getUserName(cf)).isEqualTo("user"); + assertThat(cf.getPassword()).isEqualTo("password"); + }); + } + private ContextConsumer assertClientOptions( Class expectedType, Consumer options) { return (context) -> { From ac41e3f99a8ba1224e5f00f89c08e4c9fddc3d92 Mon Sep 17 00:00:00 2001 From: facewise Date: Thu, 4 Sep 2025 09:24:38 +0900 Subject: [PATCH 2/3] Support Lettuce static master-replica auto-configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 용현 --- .../JedisConnectionConfiguration.java | 13 +-- .../LettuceConnectionConfiguration.java | 54 +++++++++++-- .../PropertiesRedisConnectionDetails.java | 38 --------- .../RedisConnectionConfiguration.java | 40 +-------- .../autoconfigure/RedisConnectionDetails.java | 56 ++----------- .../redis/autoconfigure/RedisProperties.java | 58 +++++++------ ...PropertiesRedisConnectionDetailsTests.java | 26 ------ .../RedisAutoConfigurationJedisTests.java | 9 --- .../RedisAutoConfigurationTests.java | 81 +++++++++---------- 9 files changed, 122 insertions(+), 253 deletions(-) diff --git a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/JedisConnectionConfiguration.java b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/JedisConnectionConfiguration.java index 6624ada8d1cc..8ff0da512d5d 100644 --- a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/JedisConnectionConfiguration.java +++ b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/JedisConnectionConfiguration.java @@ -38,7 +38,6 @@ 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.jedis.JedisClientConfiguration; import org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisClientConfigurationBuilder; import org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisSslClientConfigurationBuilder; @@ -56,7 +55,6 @@ * @author Andy Wilkinson * @author Phillip Webb * @author Scott Frederick - * @author Yong-Hyun Kim */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ GenericObjectPool.class, JedisConnection.class, Jedis.class }) @@ -66,12 +64,10 @@ class JedisConnectionConfiguration extends RedisConnectionConfiguration { JedisConnectionConfiguration(RedisProperties properties, ObjectProvider standaloneConfigurationProvider, - ObjectProvider sentinelConfigurationProvider, - ObjectProvider clusterConfigurationProvider, - ObjectProvider staticMasterReplicaConfigurationProvider, - RedisConnectionDetails connectionDetails) { - super(properties, connectionDetails, standaloneConfigurationProvider, sentinelConfigurationProvider, - clusterConfigurationProvider, staticMasterReplicaConfigurationProvider); + ObjectProvider sentinelConfiguration, + ObjectProvider clusterConfiguration, RedisConnectionDetails connectionDetails) { + super(properties, connectionDetails, standaloneConfigurationProvider, sentinelConfiguration, + clusterConfiguration); } @Bean @@ -107,7 +103,6 @@ private JedisConnectionFactory createJedisConnectionFactory( Assert.state(sentinelConfig != null, "'sentinelConfig' must not be null"); yield new JedisConnectionFactory(sentinelConfig, clientConfiguration); } - case STATIC_MASTER_REPLICA -> throw new IllegalStateException("Static master-replica mode is not supported by Jedis"); }; } 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 b9c2561235b2..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; @@ -55,6 +59,7 @@ 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; /** @@ -76,10 +81,9 @@ class LettuceConnectionConfiguration extends RedisConnectionConfiguration { ObjectProvider standaloneConfigurationProvider, ObjectProvider sentinelConfigurationProvider, ObjectProvider clusterConfigurationProvider, - ObjectProvider staticMasterReplicaConfigurationProvider, RedisConnectionDetails connectionDetails) { super(properties, connectionDetails, standaloneConfigurationProvider, sentinelConfigurationProvider, - clusterConfigurationProvider, staticMasterReplicaConfigurationProvider); + clusterConfigurationProvider); } @Bean(destroyMethod = "shutdown") @@ -123,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,15 +145,29 @@ private LettuceConnectionFactory createConnectionFactory( Assert.state(sentinelConfig != null, "'sentinelConfig' must not be null"); yield new LettuceConnectionFactory(sentinelConfig, clientConfiguration); } - case STATIC_MASTER_REPLICA -> { - RedisStaticMasterReplicaConfiguration staticMasterReplicaConfiguration = getStaticMasterReplicaConfiguration(); - Assert.state(staticMasterReplicaConfiguration != null, - "'staticMasterReplicaConfiguration' must not be null"); - yield new LettuceConnectionFactory(staticMasterReplicaConfiguration, clientConfiguration); - } }; } + 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, @@ -259,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/PropertiesRedisConnectionDetails.java b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/PropertiesRedisConnectionDetails.java index 6558f6b1242a..b1328a42492b 100644 --- a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/PropertiesRedisConnectionDetails.java +++ b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/PropertiesRedisConnectionDetails.java @@ -35,7 +35,6 @@ * @author Scott Frederick * @author Yanming Zhou * @author Phillip Webb - * @author Yong-Hyun Kim */ class PropertiesRedisConnectionDetails implements RedisConnectionDetails { @@ -93,12 +92,6 @@ public Standalone getStandalone() { return (cluster != null) ? new PropertiesCluster(cluster) : null; } - @Override - public @Nullable StaticMasterReplica getStaticMasterReplica() { - RedisProperties.StaticMasterReplica staticMasterReplica = this.properties.getStaticMasterReplica(); - return (staticMasterReplica != null) ? new PropertiesStaticMasterReplica(getStandalone().getDatabase(), staticMasterReplica) : null; - } - private @Nullable RedisUrl getRedisUrl() { return RedisUrl.of(this.properties.getUrl()); } @@ -188,35 +181,4 @@ public List getNodes() { } - /** - * {@link StaticMasterReplica} implementation backed by properties. - */ - private class PropertiesStaticMasterReplica implements StaticMasterReplica { - - private final int database; - - private final RedisProperties.StaticMasterReplica properties; - - PropertiesStaticMasterReplica(int database, RedisProperties.StaticMasterReplica properties) { - this.database = database; - this.properties = properties; - } - - @Override - public int getDatabase() { - return this.database; - } - - @Override - public List getNodes() { - return asNodes(this.properties.getNodes()); - } - - @Override - public @Nullable SslBundle getSslBundle() { - return PropertiesRedisConnectionDetails.this.getSslBundle(); - } - - } - } diff --git a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisConnectionConfiguration.java b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisConnectionConfiguration.java index faf2e51b17c3..024d7519b249 100644 --- a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisConnectionConfiguration.java +++ b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisConnectionConfiguration.java @@ -33,7 +33,6 @@ import org.springframework.data.redis.connection.RedisPassword; import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; -import org.springframework.data.redis.connection.RedisStaticMasterReplicaConfiguration; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -49,7 +48,6 @@ * @author Andy Wilkinson * @author Phillip Webb * @author Yanming Zhou - * @author Yong-Hyun Kim */ abstract class RedisConnectionConfiguration { @@ -64,8 +62,6 @@ abstract class RedisConnectionConfiguration { private final @Nullable RedisClusterConfiguration clusterConfiguration; - private final @Nullable RedisStaticMasterReplicaConfiguration staticMasterReplicaConfiguration; - private final RedisConnectionDetails connectionDetails; protected final Mode mode; @@ -73,14 +69,12 @@ abstract class RedisConnectionConfiguration { protected RedisConnectionConfiguration(RedisProperties properties, RedisConnectionDetails connectionDetails, ObjectProvider standaloneConfigurationProvider, ObjectProvider sentinelConfigurationProvider, - ObjectProvider clusterConfigurationProvider, - ObjectProvider staticMasterReplicaConfigurationProvider) { + ObjectProvider clusterConfigurationProvider) { this.properties = properties; this.standaloneConfiguration = standaloneConfigurationProvider.getIfAvailable(); this.sentinelConfiguration = sentinelConfigurationProvider.getIfAvailable(); this.clusterConfiguration = clusterConfigurationProvider.getIfAvailable(); this.connectionDetails = connectionDetails; - this.staticMasterReplicaConfiguration = staticMasterReplicaConfigurationProvider.getIfAvailable(); this.mode = determineMode(); } @@ -148,31 +142,6 @@ protected final RedisStandaloneConfiguration getStandaloneConfig() { return null; } - /** - * Create a {@link RedisStaticMasterReplicaConfiguration} if necessary. - * @return {@literal null} if no static master-replica settings are set. - */ - protected final @Nullable RedisStaticMasterReplicaConfiguration getStaticMasterReplicaConfiguration() { - if (this.staticMasterReplicaConfiguration != null) { - return this.staticMasterReplicaConfiguration; - } - if (this.connectionDetails.getStaticMasterReplica() != null) { - List nodes = this.connectionDetails.getStaticMasterReplica().getNodes(); - Assert.notEmpty(nodes, "'staticMasterReplica.nodes' must not be empty'"); - Node firstNode = nodes.get(0); - RedisStaticMasterReplicaConfiguration config = new RedisStaticMasterReplicaConfiguration(firstNode.host(), firstNode.port()); - nodes.stream().skip(1).forEach(node -> config.addNode(node.host(), node.port())); - config.setUsername(this.connectionDetails.getUsername()); - String password = this.connectionDetails.getPassword(); - if (password != null) { - config.setPassword(RedisPassword.of(password)); - } - config.setDatabase(this.connectionDetails.getStaticMasterReplica().getDatabase()); - return config; - } - return null; - } - private List getNodes(Cluster cluster) { return cluster.getNodes().stream().map(this::asRedisNode).toList(); } @@ -193,8 +162,6 @@ protected final RedisProperties getProperties() { ? this.connectionDetails.getCluster().getSslBundle() : null; case SENTINEL -> (this.connectionDetails.getSentinel() != null) ? this.connectionDetails.getSentinel().getSslBundle() : null; - case STATIC_MASTER_REPLICA -> (this.connectionDetails.getStaticMasterReplica() != null) - ? this.connectionDetails.getStaticMasterReplica().getSslBundle() : null; }; } @@ -230,15 +197,12 @@ private Mode determineMode() { if (getClusterConfiguration() != null) { return Mode.CLUSTER; } - if (getStaticMasterReplicaConfiguration() != null) { - return Mode.STATIC_MASTER_REPLICA; - } return Mode.STANDALONE; } enum Mode { - STANDALONE, CLUSTER, SENTINEL, STATIC_MASTER_REPLICA + STANDALONE, CLUSTER, SENTINEL } diff --git a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisConnectionDetails.java b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisConnectionDetails.java index 4f27a6c8d744..2a72c78eea75 100644 --- a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisConnectionDetails.java +++ b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisConnectionDetails.java @@ -29,7 +29,6 @@ * * @author Moritz Halbritter * @author Andy Wilkinson - * @author Yong-Hyun Kim * @since 4.0.0 */ public interface RedisConnectionDetails extends ConnectionDetails { @@ -51,8 +50,8 @@ public interface RedisConnectionDetails extends ConnectionDetails { } /** - * Redis standalone configuration. Mutually exclusive with {@link #getSentinel()}, - * {@link #getCluster()} and {@link #getStaticMasterReplica()}. + * Redis standalone configuration. Mutually exclusive with {@link #getSentinel()} and + * {@link #getCluster()}. * @return the Redis standalone configuration */ default @Nullable Standalone getStandalone() { @@ -60,8 +59,8 @@ public interface RedisConnectionDetails extends ConnectionDetails { } /** - * Redis sentinel configuration. Mutually exclusive with {@link #getStandalone()}, - * {@link #getCluster()} and {@link #getStaticMasterReplica()}. + * Redis sentinel configuration. Mutually exclusive with {@link #getStandalone()} and + * {@link #getCluster()}. * @return the Redis sentinel configuration */ default @Nullable Sentinel getSentinel() { @@ -69,23 +68,14 @@ public interface RedisConnectionDetails extends ConnectionDetails { } /** - * Redis cluster configuration. Mutually exclusive with {@link #getStandalone()}, - * {@link #getSentinel()} and {@link #getStaticMasterReplica()}. + * Redis cluster configuration. Mutually exclusive with {@link #getStandalone()} and + * {@link #getSentinel()}. * @return the Redis cluster configuration */ default @Nullable Cluster getCluster() { return null; } - /** - * Redis static Master / Replica configuration. Mutually exclusive with {@link #getStandalone()}, - * {@link #getSentinel()} and {@link #getCluster()}. - * @return the Redis static Master / Replica configuration - */ - default @Nullable StaticMasterReplica getStaticMasterReplica() { - return null; - } - /** * Redis standalone configuration. */ @@ -255,39 +245,7 @@ interface Cluster { } /** - * Redis static Master / Replica configuration. - */ - interface StaticMasterReplica { - - /** - * Database index used by the connection factory. - * @return the database index used by the connection factory - */ - default int getDatabase() { - return 0; - } - - /** - * List of Master and Replica nodes for the static configuration. This represents - * the nodes to be used in a static Master/Replica setup and is required to have - * at least one entry. The first node does not need to be the master, as the actual - * roles are determined by querying each node's ROLE command. - * @return the list of nodes for Master/Replica configuration - */ - List getNodes(); - - /** - * SSL bundle to use. - * @return the SSL bundle to use - */ - default @Nullable SslBundle getSslBundle() { - return null; - } - - } - - /** - * A node in a sentinel, cluster or static master-replica configuration. + * A node in a sentinel or cluster configuration. * * @param host the hostname of the node * @param port the port of the node 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 5adf0c633654..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 @@ -95,8 +95,6 @@ public class RedisProperties { private @Nullable Cluster cluster; - private @Nullable StaticMasterReplica staticMasterReplica; - private final Ssl ssl = new Ssl(); private final Jedis jedis = new Jedis(); @@ -203,14 +201,6 @@ public void setCluster(@Nullable Cluster cluster) { this.cluster = cluster; } - public @Nullable StaticMasterReplica getStaticMasterReplica() { - return this.staticMasterReplica; - } - - public void setStaticMasterReplica(@Nullable StaticMasterReplica staticMasterReplica) { - this.staticMasterReplica = staticMasterReplica; - } - public Jedis getJedis() { return this.jedis; } @@ -424,27 +414,6 @@ public void setPassword(@Nullable String password) { } - /** - * Redis 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; - } - - } - public static class Ssl { /** @@ -514,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; } @@ -538,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(); @@ -594,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/PropertiesRedisConnectionDetailsTests.java b/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/PropertiesRedisConnectionDetailsTests.java index 53a800338668..9e87ca435e0f 100644 --- a/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/PropertiesRedisConnectionDetailsTests.java +++ b/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/PropertiesRedisConnectionDetailsTests.java @@ -33,7 +33,6 @@ * * @author Scott Frederick * @author Moritz Halbritter - * @author Yong-Hyun Kim */ class PropertiesRedisConnectionDetailsTests { @@ -161,19 +160,6 @@ void sentinelIsConfigured() { assertThat(connectionDetails.getSentinel().getDatabase()).isEqualTo(5); } - @Test - void staticMasterReplicaIsConfigured() { - RedisProperties.StaticMasterReplica staticMasterReplica = new RedisProperties.StaticMasterReplica(); - staticMasterReplica.setNodes(List.of("localhost:1111", "127.0.0.1:2222", "[::1]:3333")); - this.properties.setStaticMasterReplica(staticMasterReplica); - this.properties.setDatabase(5); - PropertiesRedisConnectionDetails connectionDetails = new PropertiesRedisConnectionDetails(this.properties, - null); - assertThat(connectionDetails.getStaticMasterReplica().getNodes()).containsExactly(new Node("localhost", 1111), - new Node("127.0.0.1", 2222), new Node("[::1]", 3333)); - assertThat(connectionDetails.getStaticMasterReplica().getDatabase()).isEqualTo(5); - } - @Test void sentinelDatabaseIsConfiguredFromUrl() { RedisProperties.Sentinel sentinel = new RedisProperties.Sentinel(); @@ -186,18 +172,6 @@ void sentinelDatabaseIsConfiguredFromUrl() { assertThat(connectionDetails.getSentinel().getDatabase()).isEqualTo(9999); } - @Test - void staticMasterReplicaDatabaseIsConfiguredFromUrl() { - RedisProperties.StaticMasterReplica staticMasterReplica = new RedisProperties.StaticMasterReplica(); - staticMasterReplica.setNodes(List.of("localhost:1111", "127.0.0.1:2222", "[::1]:3333")); - this.properties.setStaticMasterReplica(staticMasterReplica); - this.properties.setUrl("redis://example.com:1234/9999"); - this.properties.setDatabase(5); - PropertiesRedisConnectionDetails connectionDetails = new PropertiesRedisConnectionDetails(this.properties, - null); - assertThat(connectionDetails.getStaticMasterReplica().getDatabase()).isEqualTo(9999); - } - @Test void shouldReturnSslBundle() { SslBundle bundle1 = mock(SslBundle.class); diff --git a/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/RedisAutoConfigurationJedisTests.java b/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/RedisAutoConfigurationJedisTests.java index cfc41012d671..bbe6f7845f8a 100644 --- a/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/RedisAutoConfigurationJedisTests.java +++ b/module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/RedisAutoConfigurationJedisTests.java @@ -49,7 +49,6 @@ * @author Andy Wilkinson * @author Phillip Webb * @author Scott Frederick - * @author Yong-Hyun Kim */ @ClassPathExclusions("lettuce-core-*.jar") class RedisAutoConfigurationJedisTests { @@ -248,14 +247,6 @@ void testRedisConfigurationWithCluster() { .isTrue()); } - @Test - void testRedisConfigurationWithStaticMasterReplica() { - this.contextRunner.withPropertyValues("spring.data.redis.static-master-replica.nodes=127.0.0.1:27379,127.0.0.1:27380") - .withUserConfiguration(JedisConnectionFactoryCaptorConfiguration.class) - .run((context) -> assertThat(JedisConnectionFactoryCaptor.connectionFactory.isRedisClusterAware()) - .isTrue()); - } - @Test void testRedisConfigurationWithSslEnabled() { this.contextRunner.withPropertyValues("spring.data.redis.ssl.enabled:true").run((context) -> { 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 f67d6390d8c0..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 @@ -61,7 +61,6 @@ import org.springframework.data.redis.connection.RedisPassword; 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.jedis.JedisConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceClientConfigurationBuilder; @@ -503,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) -> { @@ -680,50 +711,6 @@ void shouldUseVirtualThreadsIfEnabled() { }); } - @Test - void testRedisConfigurationWithStaticMasterReplica() { - this.contextRunner - .withPropertyValues("spring.data.redis.static-master-replica.nodes=127.0.0.1:27379,127.0.0.1:27380") - .run((context) -> { - LettuceConnectionFactory connectionFactory = context.getBean(LettuceConnectionFactory.class); - Object configuration = ReflectionTestUtils.getField(connectionFactory, "configuration"); - assertThat(configuration) - .isInstanceOf(RedisStaticMasterReplicaConfiguration.class); - assertThat(connectionFactory.getSentinelConfiguration()).isNull(); - assertThat(connectionFactory.getClusterConfiguration()).isNull(); - }); - } - - @Test - void testRedisConfigurationWithStaticMasterReplicaAndDatabaseAndAuthentication() { - this.contextRunner - .withPropertyValues( - "spring.data.redis.static-master-replica.nodes:127.0.0.1:27379,127.0.0.1:27380", - "spring.data.redis.database:1", - "spring.data.redis.username:user", - "spring.data.redis.password:password") - .run((context) -> { - LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); - assertThat(cf.getDatabase()).isOne(); - assertThat(getUserName(cf)).isEqualTo("user"); - assertThat(cf.getPassword()).isEqualTo("password"); - }); - } - - @Test - void testRedisConfigurationWithStaticMasterReplicaDatabaseAndCredentialsFromUrl() { - this.contextRunner - .withPropertyValues( - "spring.data.redis.url:redis://user:password@localhost:6379/5", - "spring.data.redis.static-master-replica.nodes:127.0.0.1:27379,127.0.0.1:27380") - .run((context) -> { - LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); - assertThat(cf.getDatabase()).isEqualTo(5); - assertThat(getUserName(cf)).isEqualTo("user"); - assertThat(cf.getPassword()).isEqualTo("password"); - }); - } - private ContextConsumer assertClientOptions( Class expectedType, Consumer options) { return (context) -> { @@ -750,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; From 6175d906440d51b7e14d5cbb0d7afee6eafcc0af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9A=A9=ED=98=84?= Date: Mon, 22 Sep 2025 15:39:01 +0900 Subject: [PATCH 3/3] Add static master-replica mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 용현 --- .../JedisConnectionConfiguration.java | 1 + .../LettuceConnectionConfiguration.java | 13 +++++-- .../RedisConnectionConfiguration.java | 9 ++++- .../redis/autoconfigure/RedisProperties.java | 39 +++++++------------ 4 files changed, 30 insertions(+), 32 deletions(-) diff --git a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/JedisConnectionConfiguration.java b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/JedisConnectionConfiguration.java index 8ff0da512d5d..5c7eaf3b1d05 100644 --- a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/JedisConnectionConfiguration.java +++ b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/JedisConnectionConfiguration.java @@ -103,6 +103,7 @@ private JedisConnectionFactory createJedisConnectionFactory( Assert.state(sentinelConfig != null, "'sentinelConfig' must not be null"); yield new JedisConnectionFactory(sentinelConfig, clientConfiguration); } + case STATIC_MASTER_REPLICA -> throw new IllegalStateException("Static master replica is not supported for Jedis"); }; } 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 7d4d6fc2e044..944d01357659 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 @@ -40,8 +40,8 @@ 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; 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; @@ -145,14 +145,19 @@ private LettuceConnectionFactory createConnectionFactory( Assert.state(sentinelConfig != null, "'sentinelConfig' must not be null"); yield new LettuceConnectionFactory(sentinelConfig, clientConfiguration); } + case STATIC_MASTER_REPLICA -> { + RedisStaticMasterReplicaConfiguration configuration = getStaticMasterReplicaConfiguration(); + Assert.state(configuration != null, "'staticMasterReplicaConfiguration' must not be null"); + yield new LettuceConnectionFactory(configuration, clientConfiguration); + } }; } private @Nullable RedisStaticMasterReplicaConfiguration getStaticMasterReplicaConfiguration() { - StaticMasterReplica staticMasterReplica = getProperties().getLettuce().getStaticMasterReplica(); + RedisProperties.Lettuce lettuce = getProperties().getLettuce(); - if (!CollectionUtils.isEmpty(staticMasterReplica.getNodes())) { - List nodes = asNodes(staticMasterReplica.getNodes()); + if (!CollectionUtils.isEmpty(lettuce.getNodes())) { + List nodes = asNodes(lettuce.getNodes()); RedisStaticMasterReplicaConfiguration configuration = new RedisStaticMasterReplicaConfiguration( nodes.get(0).host(), nodes.get(0).port()); configuration.setUsername(getProperties().getUsername()); diff --git a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisConnectionConfiguration.java b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisConnectionConfiguration.java index 024d7519b249..5e3b1ba09a93 100644 --- a/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisConnectionConfiguration.java +++ b/module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/RedisConnectionConfiguration.java @@ -35,6 +35,7 @@ import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; /** * Base Redis connection configuration. @@ -48,6 +49,7 @@ * @author Andy Wilkinson * @author Phillip Webb * @author Yanming Zhou + * @author Yong-Hyun Kim */ abstract class RedisConnectionConfiguration { @@ -156,7 +158,7 @@ protected final RedisProperties getProperties() { protected @Nullable SslBundle getSslBundle() { return switch (this.mode) { - case STANDALONE -> (this.connectionDetails.getStandalone() != null) + case STANDALONE, STATIC_MASTER_REPLICA -> (this.connectionDetails.getStandalone() != null) ? this.connectionDetails.getStandalone().getSslBundle() : null; case CLUSTER -> (this.connectionDetails.getCluster() != null) ? this.connectionDetails.getCluster().getSslBundle() : null; @@ -197,12 +199,15 @@ private Mode determineMode() { if (getClusterConfiguration() != null) { return Mode.CLUSTER; } + if (!CollectionUtils.isEmpty(this.properties.getLettuce().getNodes())) { + return Mode.STATIC_MASTER_REPLICA; + } return Mode.STANDALONE; } enum Mode { - STANDALONE, CLUSTER, SENTINEL + STANDALONE, CLUSTER, SENTINEL, STATIC_MASTER_REPLICA } 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 f2024da21d6d..572b17601ed9 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 @@ -483,7 +483,19 @@ public static class Lettuce { private final Cluster cluster = new Cluster(); - private final StaticMasterReplica staticMasterReplica = new StaticMasterReplica(); + /** + * List of static master-replica "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; + } public Duration getShutdownTimeout() { return this.shutdownTimeout; @@ -509,10 +521,6 @@ public Cluster getCluster() { return this.cluster; } - public StaticMasterReplica getStaticMasterReplica() { - return this.staticMasterReplica; - } - public static class Cluster { private final Refresh refresh = new Refresh(); @@ -569,27 +577,6 @@ 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; - } - - } - } }