Skip to content

Commit c280fde

Browse files
facewisesnicoll
authored andcommitted
Add support for static master-replica with Lettuce
See gh-46957 Signed-off-by: 용현 <dydguskim@gripcorp.co>
1 parent 319e462 commit c280fde

File tree

5 files changed

+111
-1
lines changed

5 files changed

+111
-1
lines changed

module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisConnectionConfiguration.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
3636
import org.springframework.util.Assert;
3737
import org.springframework.util.ClassUtils;
38+
import org.springframework.util.CollectionUtils;
3839

3940
/**
4041
* Base Redis connection configuration.
@@ -48,6 +49,7 @@
4849
* @author Andy Wilkinson
4950
* @author Phillip Webb
5051
* @author Yanming Zhou
52+
* @author Yong-Hyun Kim
5153
*/
5254
abstract class DataRedisConnectionConfiguration {
5355

@@ -191,12 +193,15 @@ private Mode determineMode() {
191193
if (getClusterConfiguration() != null) {
192194
return Mode.CLUSTER;
193195
}
196+
if (!CollectionUtils.isEmpty(this.properties.getLettuce().getNodes())) {
197+
return Mode.STATIC_MASTER_REPLICA;
198+
}
194199
return Mode.STANDALONE;
195200
}
196201

197202
enum Mode {
198203

199-
STANDALONE, CLUSTER, SENTINEL
204+
STANDALONE, CLUSTER, SENTINEL, STATIC_MASTER_REPLICA
200205

201206
}
202207

module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisProperties.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
* @author Stephane Nicoll
3535
* @author Scott Frederick
3636
* @author Yanming Zhou
37+
* @author Yong-Hyun Kim
3738
* @since 4.0.0
3839
*/
3940
@ConfigurationProperties("spring.data.redis")
@@ -482,6 +483,20 @@ public static class Lettuce {
482483

483484
private final Cluster cluster = new Cluster();
484485

486+
/**
487+
* List of static master-replica "host:port" pairs regardless of role
488+
* as the actual roles are determined by querying each node's ROLE command.
489+
*/
490+
private @Nullable List<String> nodes;
491+
492+
public @Nullable List<String> getNodes() {
493+
return this.nodes;
494+
}
495+
496+
public void setNodes(@Nullable List<String> nodes) {
497+
this.nodes = nodes;
498+
}
499+
485500
public Duration getShutdownTimeout() {
486501
return this.shutdownTimeout;
487502
}

module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/JedisConnectionConfiguration.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ private JedisConnectionFactory createJedisConnectionFactory(
104104
Assert.state(sentinelConfig != null, "'sentinelConfig' must not be null");
105105
yield new JedisConnectionFactory(sentinelConfig, clientConfiguration);
106106
}
107+
case STATIC_MASTER_REPLICA -> throw new IllegalStateException("Static master replica is not supported for Jedis");
107108
};
108109
}
109110

module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/LettuceConnectionConfiguration.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package org.springframework.boot.data.redis.autoconfigure;
1818

1919
import java.time.Duration;
20+
import java.util.Collections;
21+
import java.util.List;
2022

2123
import io.lettuce.core.ClientOptions;
2224
import io.lettuce.core.ReadFrom;
@@ -37,6 +39,8 @@
3739
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
3840
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
3941
import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading;
42+
import org.springframework.boot.data.redis.autoconfigure.RedisConnectionDetails.Node;
43+
import org.springframework.boot.data.redis.autoconfigure.RedisProperties.Lettuce;
4044
import org.springframework.boot.data.redis.autoconfigure.DataRedisProperties.Lettuce.Cluster.Refresh;
4145
import org.springframework.boot.data.redis.autoconfigure.DataRedisProperties.Pool;
4246
import org.springframework.boot.ssl.SslBundle;
@@ -49,11 +53,13 @@
4953
import org.springframework.data.redis.connection.RedisConnectionFactory;
5054
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
5155
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
56+
import org.springframework.data.redis.connection.RedisStaticMasterReplicaConfiguration;
5257
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
5358
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceClientConfigurationBuilder;
5459
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
5560
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
5661
import org.springframework.util.Assert;
62+
import org.springframework.util.CollectionUtils;
5763
import org.springframework.util.StringUtils;
5864

5965
/**
@@ -64,6 +70,7 @@
6470
* @author Moritz Halbritter
6571
* @author Phillip Webb
6672
* @author Scott Frederick
73+
* @author Yong-Hyun Kim
6774
*/
6875
@Configuration(proxyBeanMethods = false)
6976
@ConditionalOnClass(RedisClient.class)
@@ -120,6 +127,12 @@ private LettuceConnectionFactory createConnectionFactory(
120127
LettuceClientConfiguration clientConfiguration = getLettuceClientConfiguration(
121128
clientConfigurationBuilderCustomizers, clientOptionsBuilderCustomizers, clientResources,
122129
getProperties().getLettuce().getPool());
130+
131+
RedisStaticMasterReplicaConfiguration staticMasterReplicaConfiguration = getStaticMasterReplicaConfiguration();
132+
if (staticMasterReplicaConfiguration != null) {
133+
return new LettuceConnectionFactory(staticMasterReplicaConfiguration, clientConfiguration);
134+
}
135+
123136
return switch (this.mode) {
124137
case STANDALONE -> new LettuceConnectionFactory(getStandaloneConfig(), clientConfiguration);
125138
case CLUSTER -> {
@@ -132,9 +145,34 @@ private LettuceConnectionFactory createConnectionFactory(
132145
Assert.state(sentinelConfig != null, "'sentinelConfig' must not be null");
133146
yield new LettuceConnectionFactory(sentinelConfig, clientConfiguration);
134147
}
148+
case STATIC_MASTER_REPLICA -> {
149+
RedisStaticMasterReplicaConfiguration configuration = getStaticMasterReplicaConfiguration();
150+
Assert.state(configuration != null, "'staticMasterReplicaConfiguration' must not be null");
151+
yield new LettuceConnectionFactory(configuration, clientConfiguration);
152+
}
135153
};
136154
}
137155

156+
private @Nullable RedisStaticMasterReplicaConfiguration getStaticMasterReplicaConfiguration() {
157+
RedisProperties.Lettuce lettuce = getProperties().getLettuce();
158+
159+
if (!CollectionUtils.isEmpty(lettuce.getNodes())) {
160+
List<Node> nodes = asNodes(lettuce.getNodes());
161+
RedisStaticMasterReplicaConfiguration configuration = new RedisStaticMasterReplicaConfiguration(
162+
nodes.get(0).host(), nodes.get(0).port());
163+
configuration.setUsername(getProperties().getUsername());
164+
if (StringUtils.hasText(getProperties().getPassword())) {
165+
configuration.setPassword(getProperties().getPassword());
166+
}
167+
configuration.setDatabase(getProperties().getDatabase());
168+
nodes.stream().skip(1).forEach((node) -> configuration.addNode(node.host(), node.port()));
169+
170+
return configuration;
171+
}
172+
173+
return null;
174+
}
175+
138176
private LettuceClientConfiguration getLettuceClientConfiguration(
139177
ObjectProvider<LettuceClientConfigurationBuilderCustomizer> clientConfigurationBuilderCustomizers,
140178
ObjectProvider<LettuceClientOptionsBuilderCustomizer> clientOptionsBuilderCustomizers,
@@ -250,6 +288,20 @@ private void customizeConfigurationFromUrl(LettuceClientConfiguration.LettuceCli
250288
}
251289
}
252290

291+
private List<Node> asNodes(@Nullable List<String> nodes) {
292+
if (nodes == null) {
293+
return Collections.emptyList();
294+
}
295+
return nodes.stream().map(this::asNode).toList();
296+
}
297+
298+
private Node asNode(String node) {
299+
int portSeparatorIndex = node.lastIndexOf(':');
300+
String host = node.substring(0, portSeparatorIndex);
301+
int port = Integer.parseInt(node.substring(portSeparatorIndex + 1));
302+
return new Node(host, port);
303+
}
304+
253305
/**
254306
* Inner class to allow optional commons-pool2 dependency.
255307
*/

module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAutoConfigurationTests.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
* @author Moritz Halbritter
9292
* @author Andy Wilkinson
9393
* @author Phillip Webb
94+
* @author Yong-Hyun Kim
9495
*/
9596
class DataRedisAutoConfigurationTests {
9697

@@ -501,6 +502,38 @@ void testRedisConfigurationWithClusterAndAuthentication() {
501502
);
502503
}
503504

505+
@Test
506+
void testRedisConfigurationWithStaticMasterReplica() {
507+
List<String> staticMasterReplicaNodes = Arrays.asList("127.0.0.1:28319", "127.0.0.1:28320", "[::1]:28321");
508+
this.contextRunner
509+
.withPropertyValues(
510+
"spring.data.redis.lettuce.static-master-replica.nodes[0]:" + staticMasterReplicaNodes.get(0),
511+
"spring.data.redis.lettuce.static-master-replica.nodes[1]:" + staticMasterReplicaNodes.get(1),
512+
"spring.data.redis.lettuce.static-master-replica.nodes[2]:" + staticMasterReplicaNodes.get(2))
513+
.run((context) -> {
514+
LettuceConnectionFactory connectionFactory = context.getBean(LettuceConnectionFactory.class);
515+
assertThat(connectionFactory.getSentinelConfiguration()).isNull();
516+
assertThat(connectionFactory.getClusterConfiguration()).isNull();
517+
assertThat(isStaticMasterReplicaAware(connectionFactory)).isTrue();
518+
});
519+
}
520+
521+
@Test
522+
void testRedisConfigurationWithStaticMasterReplicaAndAuthenticationAndDatabase() {
523+
List<String> staticMasterReplicaNodes = Arrays.asList("127.0.0.1:28319", "127.0.0.1:28320");
524+
this.contextRunner
525+
.withPropertyValues("spring.data.redis.username=user", "spring.data.redis.password=password",
526+
"spring.data.redis.database=1",
527+
"spring.data.redis.lettuce.static-master-replica.nodes[0]:" + staticMasterReplicaNodes.get(0),
528+
"spring.data.redis.lettuce.static-master-replica.nodes[1]:" + staticMasterReplicaNodes.get(1))
529+
.run((context) -> {
530+
LettuceConnectionFactory connectionFactory = context.getBean(LettuceConnectionFactory.class);
531+
assertThat(getUserName(connectionFactory)).isEqualTo("user");
532+
assertThat(connectionFactory.getPassword()).isEqualTo("password");
533+
assertThat(connectionFactory.getDatabase()).isOne();
534+
});
535+
}
536+
504537
@Test
505538
void testRedisConfigurationCreateClientOptionsByDefault() {
506539
this.contextRunner.run(assertClientOptions(ClientOptions.class, (options) -> {
@@ -705,6 +738,10 @@ private RedisClusterNode createRedisNode(String host) {
705738
return node;
706739
}
707740

741+
private boolean isStaticMasterReplicaAware(LettuceConnectionFactory factory) {
742+
return ReflectionTestUtils.invokeMethod(factory, "isStaticMasterReplicaAware");
743+
}
744+
708745
private static final class RedisNodes implements Nodes {
709746

710747
private final List<RedisNodeDescription> descriptions;

0 commit comments

Comments
 (0)