Merge pull request #46957 from facewise

* pr/46957:
  Polish "Add support for static master-replica with Lettuce"
  Add support for static master-replica with Lettuce

Closes gh-46957
This commit is contained in:
Stéphane Nicoll 2025-10-03 10:14:29 +02:00
commit 911578e560
10 changed files with 235 additions and 13 deletions

View File

@ -75,7 +75,10 @@ spring:
TIP: You can also register an arbitrary number of beans that implement javadoc:org.springframework.boot.data.redis.autoconfigure.LettuceClientConfigurationBuilderCustomizer[] for more advanced customizations.
javadoc:io.lettuce.core.resource.ClientResources[] can also be customized using javadoc:org.springframework.boot.data.redis.autoconfigure.ClientResourcesBuilderCustomizer[].
If you use Jedis, javadoc:org.springframework.boot.data.redis.autoconfigure.JedisClientConfigurationBuilderCustomizer[] is also available.
Alternatively, you can register a bean of type javadoc:org.springframework.data.redis.connection.RedisStandaloneConfiguration[], javadoc:org.springframework.data.redis.connection.RedisSentinelConfiguration[], or javadoc:org.springframework.data.redis.connection.RedisClusterConfiguration[] to take full control over the configuration.
Alternatively, you can register a bean of type javadoc:org.springframework.data.redis.connection.RedisStandaloneConfiguration[], javadoc:org.springframework.data.redis.connection.RedisSentinelConfiguration[], javadoc:org.springframework.data.redis.connection.RedisClusterConfiguration[], or javadoc:org.springframework.data.redis.connection.RedisStaticMasterReplicaConfiguration[]] to take full control over the configuration.
NOTE: master/replica is not supported by Jedis.
If you add your own javadoc:org.springframework.context.annotation.Bean[format=annotation] of any of the auto-configured types, it replaces the default (except in the case of javadoc:org.springframework.data.redis.core.RedisTemplate[], when the exclusion is based on the bean name, `redisTemplate`, not its type).

View File

@ -33,6 +33,7 @@ import org.springframework.data.redis.connection.RedisNode;
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;
@ -62,6 +63,8 @@ abstract class DataRedisConnectionConfiguration {
private final @Nullable RedisClusterConfiguration clusterConfiguration;
private final @Nullable RedisStaticMasterReplicaConfiguration masterReplicaConfiguration;
private final DataRedisConnectionDetails connectionDetails;
protected final Mode mode;
@ -70,11 +73,13 @@ abstract class DataRedisConnectionConfiguration {
DataRedisConnectionDetails connectionDetails,
ObjectProvider<RedisStandaloneConfiguration> standaloneConfigurationProvider,
ObjectProvider<RedisSentinelConfiguration> sentinelConfigurationProvider,
ObjectProvider<RedisClusterConfiguration> clusterConfigurationProvider) {
ObjectProvider<RedisClusterConfiguration> clusterConfigurationProvider,
ObjectProvider<RedisStaticMasterReplicaConfiguration> masterReplicaConfiguration) {
this.properties = properties;
this.standaloneConfiguration = standaloneConfigurationProvider.getIfAvailable();
this.sentinelConfiguration = sentinelConfigurationProvider.getIfAvailable();
this.clusterConfiguration = clusterConfigurationProvider.getIfAvailable();
this.masterReplicaConfiguration = masterReplicaConfiguration.getIfAvailable();
this.connectionDetails = connectionDetails;
this.mode = determineMode();
}
@ -143,6 +148,25 @@ abstract class DataRedisConnectionConfiguration {
return null;
}
protected final @Nullable RedisStaticMasterReplicaConfiguration getMasterReplicaConfiguration() {
if (this.masterReplicaConfiguration != null) {
return this.masterReplicaConfiguration;
}
if (this.connectionDetails.getMasterReplica() != null) {
List<Node> nodes = this.connectionDetails.getMasterReplica().getNodes();
RedisStaticMasterReplicaConfiguration config = new RedisStaticMasterReplicaConfiguration(
nodes.get(0).host(), nodes.get(0).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));
}
return config;
}
return null;
}
private List<RedisNode> getNodes(Cluster cluster) {
return cluster.getNodes().stream().map(this::asRedisNode).toList();
}
@ -191,12 +215,15 @@ abstract class DataRedisConnectionConfiguration {
if (getClusterConfiguration() != null) {
return Mode.CLUSTER;
}
if (getMasterReplicaConfiguration() != null) {
return Mode.MASTER_REPLICA;
}
return Mode.STANDALONE;
}
enum Mode {
STANDALONE, CLUSTER, SENTINEL
STANDALONE, CLUSTER, MASTER_REPLICA, SENTINEL
}

View File

@ -58,8 +58,8 @@ public interface DataRedisConnectionDetails 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 #getMasterReplica()}.
* @return the Redis standalone configuration
*/
default @Nullable Standalone getStandalone() {
@ -67,8 +67,8 @@ public interface DataRedisConnectionDetails 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 #getMasterReplica()}.
* @return the Redis sentinel configuration
*/
default @Nullable Sentinel getSentinel() {
@ -76,14 +76,23 @@ public interface DataRedisConnectionDetails 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 #getMasterReplica()}.
* @return the Redis cluster configuration
*/
default @Nullable Cluster getCluster() {
return null;
}
/**
* Redis master replica configuration. Mutually exclusive with
* {@link #getStandalone()}, {@link #getSentinel()} and {@link #getCluster()}.
* @return the Redis master replica configuration
*/
default @Nullable MasterReplica getMasterReplica() {
return null;
}
/**
* Redis standalone configuration.
*/
@ -201,6 +210,20 @@ public interface DataRedisConnectionDetails extends ConnectionDetails {
}
/**
* Redis master replica configuration.
*/
interface MasterReplica {
/**
* Static nodes to use. This represents the full list of cluster nodes and is
* required to have at least one entry.
* @return the nodes to use
*/
List<Node> getNodes();
}
/**
* A node in a sentinel or cluster configuration.
*

View File

@ -94,6 +94,8 @@ public class DataRedisProperties {
private @Nullable Cluster cluster;
private @Nullable Masterreplica masterreplica;
private final Ssl ssl = new Ssl();
private final Jedis jedis = new Jedis();
@ -200,6 +202,14 @@ public class DataRedisProperties {
this.cluster = cluster;
}
public @Nullable Masterreplica getMasterreplica() {
return this.masterreplica;
}
public void setMasterreplica(@Nullable Masterreplica masterreplica) {
this.masterreplica = masterreplica;
}
public Jedis getJedis() {
return this.jedis;
}
@ -354,6 +364,26 @@ public class DataRedisProperties {
}
/**
* Master Replica properties.
*/
public static class Masterreplica {
/**
* Static list of "host:port" pairs to use, at least one entry is required.
*/
private @Nullable List<String> nodes;
public @Nullable List<String> getNodes() {
return this.nodes;
}
public void setNodes(@Nullable List<String> nodes) {
this.nodes = nodes;
}
}
/**
* Redis sentinel properties.
*/

View File

@ -38,6 +38,7 @@ 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.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisClientConfigurationBuilder;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisSslClientConfigurationBuilder;
@ -66,9 +67,10 @@ class JedisConnectionConfiguration extends DataRedisConnectionConfiguration {
ObjectProvider<RedisStandaloneConfiguration> standaloneConfigurationProvider,
ObjectProvider<RedisSentinelConfiguration> sentinelConfiguration,
ObjectProvider<RedisClusterConfiguration> clusterConfiguration,
ObjectProvider<RedisStaticMasterReplicaConfiguration> masterReplicaConfiguration,
DataRedisConnectionDetails connectionDetails) {
super(properties, connectionDetails, standaloneConfigurationProvider, sentinelConfiguration,
clusterConfiguration);
clusterConfiguration, masterReplicaConfiguration);
}
@Bean
@ -104,6 +106,7 @@ class JedisConnectionConfiguration extends DataRedisConnectionConfiguration {
Assert.state(sentinelConfig != null, "'sentinelConfig' must not be null");
yield new JedisConnectionFactory(sentinelConfig, clientConfiguration);
}
case MASTER_REPLICA -> throw new IllegalStateException("'masterReplicaConfig' is not supported by Jedis");
};
}

View File

@ -49,6 +49,7 @@ 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;
@ -74,9 +75,10 @@ class LettuceConnectionConfiguration extends DataRedisConnectionConfiguration {
ObjectProvider<RedisStandaloneConfiguration> standaloneConfigurationProvider,
ObjectProvider<RedisSentinelConfiguration> sentinelConfigurationProvider,
ObjectProvider<RedisClusterConfiguration> clusterConfigurationProvider,
ObjectProvider<RedisStaticMasterReplicaConfiguration> masterReplicaConfiguration,
DataRedisConnectionDetails connectionDetails) {
super(properties, connectionDetails, standaloneConfigurationProvider, sentinelConfigurationProvider,
clusterConfigurationProvider);
clusterConfigurationProvider, masterReplicaConfiguration);
}
@Bean(destroyMethod = "shutdown")
@ -132,6 +134,11 @@ class LettuceConnectionConfiguration extends DataRedisConnectionConfiguration {
Assert.state(sentinelConfig != null, "'sentinelConfig' must not be null");
yield new LettuceConnectionFactory(sentinelConfig, clientConfiguration);
}
case MASTER_REPLICA -> {
RedisStaticMasterReplicaConfiguration masterReplicaConfiguration = getMasterReplicaConfiguration();
Assert.state(masterReplicaConfiguration != null, "'masterReplicaConfig' must not be null");
yield new LettuceConnectionFactory(masterReplicaConfiguration, clientConfiguration);
}
};
}

View File

@ -92,6 +92,12 @@ class PropertiesDataRedisConnectionDetails implements DataRedisConnectionDetails
return (cluster != null) ? new PropertiesCluster(cluster) : null;
}
@Override
public @Nullable MasterReplica getMasterReplica() {
DataRedisProperties.Masterreplica masterreplica = this.properties.getMasterreplica();
return (masterreplica != null) ? new PropertiesMasterReplica(masterreplica) : null;
}
private @Nullable DataRedisUrl getRedisUrl() {
return DataRedisUrl.of(this.properties.getUrl());
}
@ -128,6 +134,24 @@ class PropertiesDataRedisConnectionDetails implements DataRedisConnectionDetails
}
/**
* {@link MasterReplica} implementation backed by properties.
*/
private class PropertiesMasterReplica implements MasterReplica {
private final List<Node> nodes;
PropertiesMasterReplica(DataRedisProperties.Masterreplica properties) {
this.nodes = asNodes(properties.getNodes());
}
@Override
public List<Node> getNodes() {
return this.nodes;
}
}
/**
* {@link Sentinel} implementation backed by properties.
*/

View File

@ -247,6 +247,15 @@ class DataRedisAutoConfigurationJedisTests {
.isTrue());
}
@Test
void testRedisConfigurationWitMasterReplica() {
this.contextRunner.withPropertyValues("spring.data.redis.masterreplica.nodes=127.0.0.1:27379,127.0.0.1:27380")
.run((context) -> assertThat(context).hasFailed()
.getFailure()
.rootCause()
.hasMessageContaining("'masterReplicaConfig' is not supported by Jedis"));
}
@Test
void testRedisConfigurationWithSslEnabled() {
this.contextRunner.withPropertyValues("spring.data.redis.ssl.enabled:true").run((context) -> {

View File

@ -61,6 +61,7 @@ import org.springframework.data.redis.connection.RedisNode;
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;
@ -496,9 +497,39 @@ class DataRedisAutoConfigurationTests {
LettuceConnectionFactory connectionFactory = context.getBean(LettuceConnectionFactory.class);
assertThat(getUserName(connectionFactory)).isEqualTo("user");
assertThat(connectionFactory.getPassword()).isEqualTo("password");
}
});
}
);
@Test
void testRedisConfigurationWithMasterReplica() {
this.contextRunner
.withPropertyValues("spring.data.redis.masterreplica.nodes=127.0.0.1:28319,127.0.0.1:28320,[::1]:28321")
.run((context) -> {
LettuceConnectionFactory connectionFactory = context.getBean(LettuceConnectionFactory.class);
assertThat(connectionFactory.getSentinelConfiguration()).isNull();
assertThat(connectionFactory.getClusterConfiguration()).isNull();
assertThat(connectionFactory).extracting("configuration")
.isInstanceOfSatisfying(RedisStaticMasterReplicaConfiguration.class,
(masterReplicaConfiguration) -> assertThat(masterReplicaConfiguration.getNodes()
.stream()
.map((config) -> new RedisNode(config.getHostName(), config.getPort())))
.containsExactly(new RedisNode("127.0.0.1", 28319), new RedisNode("127.0.0.1", 28320),
new RedisNode("[::1]", 28321)));
});
}
@Test
void testRedisConfigurationWithMasterAndAuthentication() {
this.contextRunner
.withPropertyValues("spring.data.redis.username=user", "spring.data.redis.password=password",
"spring.data.redis.masterreplica.nodes=127.0.0.1:28319,127.0.0.1:28320")
.run((context) -> {
LettuceConnectionFactory connectionFactory = context.getBean(LettuceConnectionFactory.class);
assertThat(getUserName(connectionFactory)).isEqualTo("user");
assertThat(connectionFactory.getPassword()).isEqualTo("password");
assertThat(connectionFactory).extracting("configuration")
.isInstanceOf(RedisStaticMasterReplicaConfiguration.class);
});
}
@Test
@ -628,6 +659,27 @@ class DataRedisAutoConfigurationTests {
});
}
@Test
void usesMasterReplicaFromCustomConnectionDetails() {
this.contextRunner.withUserConfiguration(ConnectionDetailsMasterReplicaConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(DataRedisConnectionDetails.class)
.doesNotHaveBean(PropertiesDataRedisConnectionDetails.class);
LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class);
assertThat(cf.isUseSsl()).isFalse();
assertThat(cf).extracting("configuration")
.isInstanceOfSatisfying(RedisStaticMasterReplicaConfiguration.class,
(masterReplicationConfiguration) -> {
assertThat(masterReplicationConfiguration.getUsername()).isEqualTo("user-1");
assertThat(masterReplicationConfiguration.getPassword().get())
.isEqualTo("password-1".toCharArray());
assertThat(masterReplicationConfiguration.getNodes())
.map((nodeConfiguration) -> new RedisNode(nodeConfiguration.getHostName(),
nodeConfiguration.getPort()))
.containsExactly(new RedisNode("node-1", 12345), new RedisNode("node-2", 23456));
});
});
}
@Test
void testRedisConfigurationWithSslEnabled() {
this.contextRunner.withPropertyValues("spring.data.redis.ssl.enabled:true").run((context) -> {
@ -884,4 +936,38 @@ class DataRedisAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class ConnectionDetailsMasterReplicaConfiguration {
@Bean
DataRedisConnectionDetails redisConnectionDetails() {
return new DataRedisConnectionDetails() {
@Override
public String getUsername() {
return "user-1";
}
@Override
public String getPassword() {
return "password-1";
}
@Override
public MasterReplica getMasterReplica() {
return new MasterReplica() {
@Override
public List<Node> getNodes() {
return List.of(new Node("node-1", 12345), new Node("node-2", 23456));
}
};
}
};
}
}
}

View File

@ -57,6 +57,7 @@ class PropertiesRedisConnectionDetailsTests {
assertThat(standalone.getDatabase()).isEqualTo(0);
assertThat(this.connectionDetails.getSentinel()).isNull();
assertThat(this.connectionDetails.getCluster()).isNull();
assertThat(this.connectionDetails.getMasterReplica()).isNull();
assertThat(this.connectionDetails.getUsername()).isNull();
assertThat(this.connectionDetails.getPassword()).isNull();
}
@ -147,6 +148,15 @@ class PropertiesRedisConnectionDetailsTests {
new Node("127.0.0.1", 2222), new Node("[::1]", 3333));
}
@Test
void masterReplicaIsConfigured() {
DataRedisProperties.Masterreplica masterReplica = new DataRedisProperties.Masterreplica();
masterReplica.setNodes(List.of("localhost:1111", "127.0.0.1:2222", "[::1]:3333"));
this.properties.setMasterreplica(masterReplica);
assertThat(this.connectionDetails.getMasterReplica().getNodes()).containsExactly(new Node("localhost", 1111),
new Node("127.0.0.1", 2222), new Node("[::1]", 3333));
}
@Test
void sentinelIsConfigured() {
DataRedisProperties.Sentinel sentinel = new DataRedisProperties.Sentinel();