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; @@ -35,6 +35,7 @@ import org.springframework.data.redis.connection.RedisSentinelConfiguration;
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 @@ import org.springframework.util.ClassUtils; @@ -48,6 +49,7 @@ import org.springframework.util.ClassUtils;
* @author Andy Wilkinson
* @author Phillip Webb
* @author Yanming Zhou
* @author Yong-Hyun Kim
*/
abstract class DataRedisConnectionConfiguration {
@ -191,12 +193,15 @@ abstract class DataRedisConnectionConfiguration { @@ -191,12 +193,15 @@ abstract class DataRedisConnectionConfiguration {
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
}

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; @@ -34,6 +34,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
* @author Stephane Nicoll
* @author Scott Frederick
* @author Yanming Zhou
* @author Yong-Hyun Kim
* @since 4.0.0
*/
@ConfigurationProperties("spring.data.redis")
@ -482,6 +483,20 @@ public class DataRedisProperties { @@ -482,6 +483,20 @@ public class DataRedisProperties {
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() {
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 { @@ -104,6 +104,7 @@ class JedisConnectionConfiguration extends DataRedisConnectionConfiguration {
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");
};
}

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

@ -17,6 +17,8 @@ @@ -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,6 +39,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -37,6 +39,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
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;
import org.springframework.boot.data.redis.autoconfigure.DataRedisProperties.Lettuce.Cluster.Refresh;
import org.springframework.boot.data.redis.autoconfigure.DataRedisProperties.Pool;
import org.springframework.boot.ssl.SslBundle;
@ -49,11 +53,13 @@ import org.springframework.data.redis.connection.RedisClusterConfiguration; @@ -49,11 +53,13 @@ import org.springframework.data.redis.connection.RedisClusterConfiguration;
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 @@ import org.springframework.util.StringUtils; @@ -64,6 +70,7 @@ import org.springframework.util.StringUtils;
* @author Moritz Halbritter
* @author Phillip Webb
* @author Scott Frederick
* @author Yong-Hyun Kim
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisClient.class)
@ -120,6 +127,12 @@ class LettuceConnectionConfiguration extends DataRedisConnectionConfiguration { @@ -120,6 +127,12 @@ class LettuceConnectionConfiguration extends DataRedisConnectionConfiguration {
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 -> {
@ -132,9 +145,34 @@ class LettuceConnectionConfiguration extends DataRedisConnectionConfiguration { @@ -132,9 +145,34 @@ class LettuceConnectionConfiguration extends DataRedisConnectionConfiguration {
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() {
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(
ObjectProvider<LettuceClientConfigurationBuilderCustomizer> clientConfigurationBuilderCustomizers,
ObjectProvider<LettuceClientOptionsBuilderCustomizer> clientOptionsBuilderCustomizers,
@ -250,6 +288,20 @@ class LettuceConnectionConfiguration extends DataRedisConnectionConfiguration { @@ -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.
*/

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

Loading…
Cancel
Save