Spring 使用 Redis Sentinel 的引导缓存始终连接到主节点
Spring Boot Caching with Redis Sentinel always connects to master node
我有一个 Spring Boot (2.3.1.RELEASE) 应用程序,它使用 Redis Sentinel 进行缓存。
这是我的 Sentinel 连接配置:
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
.master(redisProperties.getSentinel().getMaster());
redisProperties.getSentinel().getNodes().forEach(s -> sentinelConfig.sentinel(s, redisProperties.getPort()));
sentinelConfig.setPassword(RedisPassword.of(redisProperties.getPassword()));
return new LettuceConnectionFactory(sentinelConfig);
}
这是我的缓存管理器配置:
@Bean
public RedisCacheManager cacheManager() {
Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
cacheConfigs.put("cache1", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(ttlMinutes)));
cacheConfigs.put("cache2", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(ttlMinutes)));
return RedisCacheManager.builder(redisConnectionFactory())
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(ttlMinutes)))
.withInitialCacheConfigurations(cacheConfigs)
.transactionAware()
.build();
}
从缓存的角度来看一切正常。
但是,如果我在 io.lettuce.core.protocol.CommandHandler
中打开调试日志,我会发现它始终连接到同一个节点(主节点)。我可以通过查看节点上的日志来确认。
我在网上到处查看,这似乎是正确的配置。
这引出了我的问题:
- 是否可以将 Spring 缓存抽象配置为仅使用主节点进行写入而使用从节点进行读取?
这种期望是否有效?或者这就是 Sentinel 应该使用的方式(所有请求都交给 master)?
是的,可以做到。
来自 Spring Data Redis Docs - 10.4.4. Write to Master, Read from Replica :
It is said that Spring Data Redis provides a Redis Master/Replica setup which not
only allows data to be safely stored at more nodes but also allows
reading data from replicas while pushing writes to the master by using
Lettuce.
为此,您必须更新配置中的 redisConnectionFactory()
方法 class :
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.readFrom(ReadFrom.REPLICA_PREFERRED)
.build();
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
.master(redisProperties.getSentinel().getMaster());
redisProperties.getSentinel().getNodes().forEach(s -> sentinelConfig.sentinel(s, redisProperties.getPort()));
sentinelConfig.setPassword(RedisPassword.of(redisProperties.getPassword()));
return new LettuceConnectionFactory(sentinelConfig, clientConfig);
}
如果你想写master只读all slaves(replicas),你可以使用下面的配置。
这些配置包括连接池、写入主节点和从具有循环负载平衡的所有从属节点读取。
@Bean
public RedisConnectionFactory lettuceConnectionFactory(RedisProperties redisProperties, LettucePoolingClientConfiguration lettucePoolingClientConfiguration) {
final RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration().master(redisProperties.getMaster());
redisSentinelConfiguration.setDatabase(redisProperties.getDbIndex());
addSentinels(redisProperties, redisSentinelConfiguration);
return new LettuceConnectionFactory(redisSentinelConfiguration, lettucePoolingClientConfiguration);
}
private void addSentinels(RedisProperties redisProperties, RedisSentinelConfiguration redisSentinelConfiguration) {
redisProperties.getNodes()
.forEach(node -> {
final String[] splitted = node.split(NODE_SPLITTER);
final String host = splitted[0];
final int port = Integer.parseInt(splitted[1]);
redisSentinelConfiguration.addSentinel(RedisNode.newRedisNode()
.listeningAt(host, port)
.build());
});
}
@Bean
public LettucePoolingClientConfiguration lettucePoolingClientConfiguration(ClientOptions clientOptions, ClientResources clientResources, RedisProperties redisProperties) {
return LettucePoolingClientConfiguration.builder()
.readFrom(ReadFrom.ANY_REPLICA)
.poolConfig(genericObjectPoolConfig(redisProperties))
.clientOptions(clientOptions)
.clientResources(clientResources)
.build();
}
@Bean
public GenericObjectPoolConfig genericObjectPoolConfig(RedisProperties redisProperties) {
final GenericObjectPoolConfig config = new GenericObjectPoolConfig();
config.setMaxIdle(redisProperties.getPoolMaxIdle());
config.setMinIdle(redisProperties.getPoolMinIdle());
config.setMaxTotal(redisProperties.getPoolMaxTotal());
config.setBlockWhenExhausted(false);
config.setMaxWaitMillis(redisProperties.getPoolMaxWaitMillis());
return config;
}
@Bean
public ClientOptions clientOptions(RedisProperties redisProperties) {
return ClientOptions.builder()
.autoReconnect(true)
.disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS)
.timeoutOptions(TimeoutOptions.builder().fixedTimeout(Duration.ofSeconds(redisProperties.getCommandTimedOutSec())).build())
.build();
}
@Bean(destroyMethod = "shutdown")
public ClientResources clientResources() {
return DefaultClientResources.create();
}
你应该为负载平衡读取模式导入新的 lettuce 核心版本(ReadFrom.ANY_REPLICA),它会附带 spring-boot 2.4.0
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.0.1.RELEASE</version>
</dependency>
我有一个 Spring Boot (2.3.1.RELEASE) 应用程序,它使用 Redis Sentinel 进行缓存。 这是我的 Sentinel 连接配置:
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
.master(redisProperties.getSentinel().getMaster());
redisProperties.getSentinel().getNodes().forEach(s -> sentinelConfig.sentinel(s, redisProperties.getPort()));
sentinelConfig.setPassword(RedisPassword.of(redisProperties.getPassword()));
return new LettuceConnectionFactory(sentinelConfig);
}
这是我的缓存管理器配置:
@Bean
public RedisCacheManager cacheManager() {
Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
cacheConfigs.put("cache1", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(ttlMinutes)));
cacheConfigs.put("cache2", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(ttlMinutes)));
return RedisCacheManager.builder(redisConnectionFactory())
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(ttlMinutes)))
.withInitialCacheConfigurations(cacheConfigs)
.transactionAware()
.build();
}
从缓存的角度来看一切正常。
但是,如果我在 io.lettuce.core.protocol.CommandHandler
中打开调试日志,我会发现它始终连接到同一个节点(主节点)。我可以通过查看节点上的日志来确认。
我在网上到处查看,这似乎是正确的配置。
这引出了我的问题:
- 是否可以将 Spring 缓存抽象配置为仅使用主节点进行写入而使用从节点进行读取?
这种期望是否有效?或者这就是 Sentinel 应该使用的方式(所有请求都交给 master)?
是的,可以做到。
来自 Spring Data Redis Docs - 10.4.4. Write to Master, Read from Replica :
It is said that Spring Data Redis provides a Redis Master/Replica setup which not only allows data to be safely stored at more nodes but also allows reading data from replicas while pushing writes to the master by using Lettuce.
为此,您必须更新配置中的 redisConnectionFactory()
方法 class :
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.readFrom(ReadFrom.REPLICA_PREFERRED)
.build();
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
.master(redisProperties.getSentinel().getMaster());
redisProperties.getSentinel().getNodes().forEach(s -> sentinelConfig.sentinel(s, redisProperties.getPort()));
sentinelConfig.setPassword(RedisPassword.of(redisProperties.getPassword()));
return new LettuceConnectionFactory(sentinelConfig, clientConfig);
}
如果你想写master只读all slaves(replicas),你可以使用下面的配置。
这些配置包括连接池、写入主节点和从具有循环负载平衡的所有从属节点读取。
@Bean
public RedisConnectionFactory lettuceConnectionFactory(RedisProperties redisProperties, LettucePoolingClientConfiguration lettucePoolingClientConfiguration) {
final RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration().master(redisProperties.getMaster());
redisSentinelConfiguration.setDatabase(redisProperties.getDbIndex());
addSentinels(redisProperties, redisSentinelConfiguration);
return new LettuceConnectionFactory(redisSentinelConfiguration, lettucePoolingClientConfiguration);
}
private void addSentinels(RedisProperties redisProperties, RedisSentinelConfiguration redisSentinelConfiguration) {
redisProperties.getNodes()
.forEach(node -> {
final String[] splitted = node.split(NODE_SPLITTER);
final String host = splitted[0];
final int port = Integer.parseInt(splitted[1]);
redisSentinelConfiguration.addSentinel(RedisNode.newRedisNode()
.listeningAt(host, port)
.build());
});
}
@Bean
public LettucePoolingClientConfiguration lettucePoolingClientConfiguration(ClientOptions clientOptions, ClientResources clientResources, RedisProperties redisProperties) {
return LettucePoolingClientConfiguration.builder()
.readFrom(ReadFrom.ANY_REPLICA)
.poolConfig(genericObjectPoolConfig(redisProperties))
.clientOptions(clientOptions)
.clientResources(clientResources)
.build();
}
@Bean
public GenericObjectPoolConfig genericObjectPoolConfig(RedisProperties redisProperties) {
final GenericObjectPoolConfig config = new GenericObjectPoolConfig();
config.setMaxIdle(redisProperties.getPoolMaxIdle());
config.setMinIdle(redisProperties.getPoolMinIdle());
config.setMaxTotal(redisProperties.getPoolMaxTotal());
config.setBlockWhenExhausted(false);
config.setMaxWaitMillis(redisProperties.getPoolMaxWaitMillis());
return config;
}
@Bean
public ClientOptions clientOptions(RedisProperties redisProperties) {
return ClientOptions.builder()
.autoReconnect(true)
.disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS)
.timeoutOptions(TimeoutOptions.builder().fixedTimeout(Duration.ofSeconds(redisProperties.getCommandTimedOutSec())).build())
.build();
}
@Bean(destroyMethod = "shutdown")
public ClientResources clientResources() {
return DefaultClientResources.create();
}
你应该为负载平衡读取模式导入新的 lettuce 核心版本(ReadFrom.ANY_REPLICA),它会附带 spring-boot 2.4.0
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.0.1.RELEASE</version>
</dependency>