Browse Source

Add support for static master-replica with Lettuce

See gh-46957

Signed-off-by: 용현 <dydguskim@gripcorp.co>
pull/47395/head
facewise 4 months ago committed by Stéphane Nicoll
parent
commit
c280fdefe8
  1. 7
      module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisConnectionConfiguration.java
  2. 15
      module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/DataRedisProperties.java
  3. 1
      module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/JedisConnectionConfiguration.java
  4. 52
      module/spring-boot-data-redis/src/main/java/org/springframework/boot/data/redis/autoconfigure/LettuceConnectionConfiguration.java
  5. 37
      module/spring-boot-data-redis/src/test/java/org/springframework/boot/data/redis/autoconfigure/DataRedisAutoConfigurationTests.java

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

@ -35,6 +35,7 @@ import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
/** /**
* Base Redis connection configuration. * Base Redis connection configuration.
@ -48,6 +49,7 @@ import org.springframework.util.ClassUtils;
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb * @author Phillip Webb
* @author Yanming Zhou * @author Yanming Zhou
* @author Yong-Hyun Kim
*/ */
abstract class DataRedisConnectionConfiguration { abstract class DataRedisConnectionConfiguration {
@ -191,12 +193,15 @@ abstract class DataRedisConnectionConfiguration {
if (getClusterConfiguration() != null) { if (getClusterConfiguration() != null) {
return Mode.CLUSTER; return Mode.CLUSTER;
} }
if (!CollectionUtils.isEmpty(this.properties.getLettuce().getNodes())) {
return Mode.STATIC_MASTER_REPLICA;
}
return Mode.STANDALONE; return Mode.STANDALONE;
} }
enum Mode { enum Mode {
STANDALONE, CLUSTER, SENTINEL STANDALONE, CLUSTER, SENTINEL, STATIC_MASTER_REPLICA
} }

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

@ -34,6 +34,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Scott Frederick * @author Scott Frederick
* @author Yanming Zhou * @author Yanming Zhou
* @author Yong-Hyun Kim
* @since 4.0.0 * @since 4.0.0
*/ */
@ConfigurationProperties("spring.data.redis") @ConfigurationProperties("spring.data.redis")
@ -482,6 +483,20 @@ public class DataRedisProperties {
private final Cluster cluster = new Cluster(); private final Cluster cluster = new Cluster();
/**
* 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<String> nodes;
public @Nullable List<String> getNodes() {
return this.nodes;
}
public void setNodes(@Nullable List<String> nodes) {
this.nodes = nodes;
}
public Duration getShutdownTimeout() { public Duration getShutdownTimeout() {
return this.shutdownTimeout; return this.shutdownTimeout;
} }

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

@ -104,6 +104,7 @@ class JedisConnectionConfiguration extends DataRedisConnectionConfiguration {
Assert.state(sentinelConfig != null, "'sentinelConfig' must not be null"); Assert.state(sentinelConfig != null, "'sentinelConfig' must not be null");
yield new JedisConnectionFactory(sentinelConfig, clientConfiguration); yield new JedisConnectionFactory(sentinelConfig, clientConfiguration);
} }
case STATIC_MASTER_REPLICA -> throw new IllegalStateException("Static master replica is not supported for Jedis");
}; };
} }

52
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; package org.springframework.boot.data.redis.autoconfigure;
import java.time.Duration; import java.time.Duration;
import java.util.Collections;
import java.util.List;
import io.lettuce.core.ClientOptions; import io.lettuce.core.ClientOptions;
import io.lettuce.core.ReadFrom; import io.lettuce.core.ReadFrom;
@ -37,6 +39,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading; 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.DataRedisProperties.Lettuce.Cluster.Refresh; import org.springframework.boot.data.redis.autoconfigure.DataRedisProperties.Lettuce.Cluster.Refresh;
import org.springframework.boot.data.redis.autoconfigure.DataRedisProperties.Pool; import org.springframework.boot.data.redis.autoconfigure.DataRedisProperties.Pool;
import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundle;
@ -49,11 +53,13 @@ import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration; 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;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceClientConfigurationBuilder; import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceClientConfigurationBuilder;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration; import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
/** /**
@ -64,6 +70,7 @@ import org.springframework.util.StringUtils;
* @author Moritz Halbritter * @author Moritz Halbritter
* @author Phillip Webb * @author Phillip Webb
* @author Scott Frederick * @author Scott Frederick
* @author Yong-Hyun Kim
*/ */
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisClient.class) @ConditionalOnClass(RedisClient.class)
@ -120,6 +127,12 @@ class LettuceConnectionConfiguration extends DataRedisConnectionConfiguration {
LettuceClientConfiguration clientConfiguration = getLettuceClientConfiguration( LettuceClientConfiguration clientConfiguration = getLettuceClientConfiguration(
clientConfigurationBuilderCustomizers, clientOptionsBuilderCustomizers, clientResources, clientConfigurationBuilderCustomizers, clientOptionsBuilderCustomizers, clientResources,
getProperties().getLettuce().getPool()); getProperties().getLettuce().getPool());
RedisStaticMasterReplicaConfiguration staticMasterReplicaConfiguration = getStaticMasterReplicaConfiguration();
if (staticMasterReplicaConfiguration != null) {
return new LettuceConnectionFactory(staticMasterReplicaConfiguration, clientConfiguration);
}
return switch (this.mode) { return switch (this.mode) {
case STANDALONE -> new LettuceConnectionFactory(getStandaloneConfig(), clientConfiguration); case STANDALONE -> new LettuceConnectionFactory(getStandaloneConfig(), clientConfiguration);
case CLUSTER -> { case CLUSTER -> {
@ -132,9 +145,34 @@ class LettuceConnectionConfiguration extends DataRedisConnectionConfiguration {
Assert.state(sentinelConfig != null, "'sentinelConfig' must not be null"); Assert.state(sentinelConfig != null, "'sentinelConfig' must not be null");
yield new LettuceConnectionFactory(sentinelConfig, clientConfiguration); 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() {
RedisProperties.Lettuce lettuce = getProperties().getLettuce();
if (!CollectionUtils.isEmpty(lettuce.getNodes())) {
List<Node> nodes = asNodes(lettuce.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( private LettuceClientConfiguration getLettuceClientConfiguration(
ObjectProvider<LettuceClientConfigurationBuilderCustomizer> clientConfigurationBuilderCustomizers, ObjectProvider<LettuceClientConfigurationBuilderCustomizer> clientConfigurationBuilderCustomizers,
ObjectProvider<LettuceClientOptionsBuilderCustomizer> clientOptionsBuilderCustomizers, ObjectProvider<LettuceClientOptionsBuilderCustomizer> clientOptionsBuilderCustomizers,
@ -250,6 +288,20 @@ class LettuceConnectionConfiguration extends DataRedisConnectionConfiguration {
} }
} }
private List<Node> asNodes(@Nullable List<String> 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. * Inner class to allow optional commons-pool2 dependency.
*/ */

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

@ -91,6 +91,7 @@ import static org.mockito.Mockito.mock;
* @author Moritz Halbritter * @author Moritz Halbritter
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb * @author Phillip Webb
* @author Yong-Hyun Kim
*/ */
class DataRedisAutoConfigurationTests { class DataRedisAutoConfigurationTests {
@ -501,6 +502,38 @@ class DataRedisAutoConfigurationTests {
); );
} }
@Test
void testRedisConfigurationWithStaticMasterReplica() {
List<String> 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<String> 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 @Test
void testRedisConfigurationCreateClientOptionsByDefault() { void testRedisConfigurationCreateClientOptionsByDefault() {
this.contextRunner.run(assertClientOptions(ClientOptions.class, (options) -> { this.contextRunner.run(assertClientOptions(ClientOptions.class, (options) -> {
@ -705,6 +738,10 @@ class DataRedisAutoConfigurationTests {
return node; return node;
} }
private boolean isStaticMasterReplicaAware(LettuceConnectionFactory factory) {
return ReflectionTestUtils.invokeMethod(factory, "isStaticMasterReplicaAware");
}
private static final class RedisNodes implements Nodes { private static final class RedisNodes implements Nodes {
private final List<RedisNodeDescription> descriptions; private final List<RedisNodeDescription> descriptions;

Loading…
Cancel
Save