問題
redis使用過程中,很多情況都是讀多寫少,而不管是主從、哨兵、叢集,從節點都隻是用來備份,為了最大化節約使用者成本,我們需要利用從節點來進行讀,分擔主節點壓力,這裡我們繼續上一章的jedis的讀寫分離,由于springboot現在redis叢集預設用的是lettuce,是以介紹下lettuce讀寫分離
讀寫分離
主從讀寫分離
這裡先建一個主從叢集,1主3從,一般情況下隻需要進行相關配置如下:
spring:
redis:
host: redisMastHost
port: 6379
lettuce:
pool:
max-active: 512
max-idle: 256
min-idle: 256
max-wait: -1
複制
這樣就可以直接注入redisTemplate,讀寫資料了,但是這個預設隻能讀寫主,如果需要設定readfrom,則需要自定義factory,下面給出兩種方案
方案一(适用于非aws)
隻需要配置主節點,從節點會資訊會自動從主節點擷取
@Configuration
class WriteToMasterReadFromReplicaConfiguration {
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.readFrom(ReadFrom.SLAVE_PREFERRED)
.build();
RedisStandaloneConfiguration serverConfig = new RedisStandaloneConfiguration("server", 6379);
return new LettuceConnectionFactory(serverConfig, clientConfig);
}
}
複制
方案二(雲上redis,比如aws)
下面給個demo
import io.lettuce.core.ReadFrom;
import io.lettuce.core.models.role.RedisNodeDescription;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStaticMasterReplicaConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
@Configuration
public class RedisConfig {
@Value("${spring.redis1.master}")
private String master;
@Value("${spring.redis1.slaves:}")
private String slaves;
@Value("${spring.redis1.port}")
private int port;
@Value("${spring.redis1.timeout:200}")
private long timeout;
@Value("${spring.redis1.lettuce.pool.max-idle:256}")
private int maxIdle;
@Value("${spring.redis1.lettuce.pool.min-idle:256}")
private int minIdle;
@Value("${spring.redis1.lettuce.pool.max-active:512}")
private int maxActive;
@Value("${spring.redis1.lettuce.pool.max-wait:-1}")
private long maxWait;
private static Logger logger = LoggerFactory.getLogger(RedisConfig.class);
private final AtomicInteger index = new AtomicInteger(-1);
@Bean(value = "lettuceConnectionFactory1")
LettuceConnectionFactory lettuceConnectionFactory1(GenericObjectPoolConfig genericObjectPoolConfig) {
RedisStaticMasterReplicaConfiguration configuration = new RedisStaticMasterReplicaConfiguration(
this.master, this.port);
if(StringUtils.isNotBlank(slaves)){
String[] slaveHosts=slaves.split(",");
for (int i=0;i<slavehosts.length;i++){ configuration.addnode(slavehosts[i], this.port); } lettuceclientconfiguration clientconfig="LettucePoolingClientConfiguration.builder().readFrom(ReadFrom.SLAVE).commandTimeout(Duration.ofMillis(timeout))" .poolconfig(genericobjectpoolconfig).build(); return new lettuceconnectionfactory(configuration, clientconfig); ** * genericobjectpoolconfig 連接配接池配置 @return @bean public genericobjectpoolconfig() { genericobjectpoolconfig="new" genericobjectpoolconfig(); genericobjectpoolconfig.setmaxidle(maxidle); genericobjectpoolconfig.setminidle(minidle); genericobjectpoolconfig.setmaxtotal(maxactive); genericobjectpoolconfig.setmaxwaitmillis(maxwait); genericobjectpoolconfig; @bean(name="redisTemplate1" ) redistemplate redistemplate(@qualifier("lettuceconnectionfactory1") lettuceconnectionfactory connectionfactory) redistemplate<string,string> template = new RedisTemplate<string,string>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new StringRedisSerializer());
logger.info("redis 連接配接成功");
return template;
}
}
複制
這裡的核心代碼在readfrom的設定,lettuce提供了5中選項,分别是
- MASTER
- MASTER_PREFERRED
- SLAVE_PREFERRED
- SLAVE
- NEAREST 最新的版本SLAVE改成了ReadFrom.REPLICA 這裡設定為SlAVE,那麼讀請求都會走從節點,但是這裡有個bug,每次都會讀取最後一個從節點,其他從節點都不會有請求過去,跟蹤源代碼發現節點順序是一定的,但是每次getConnection時每次都會擷取最後一個,下面是緩存指令情況

解決方案就是自定義一個readFrom,如下
LettuceClientConfiguration clientConfig =
LettucePoolingClientConfiguration.builder().readFrom(new ReadFrom() {
@Override
public List<redisnodedescription> select(Nodes nodes) {
List<redisnodedescription> allNodes = nodes.getNodes();
int ind = Math.abs(index.incrementAndGet() % allNodes.size());
RedisNodeDescription selected = allNodes.get(ind);
logger.info("Selected random node {} with uri {}", ind, selected.getUri());
List<redisnodedescription> remaining = IntStream.range(0, allNodes.size())
.filter(i -> i != ind)
.mapToObj(allNodes::get).collect(Collectors.toList());
return Stream.concat(
Stream.of(selected),
remaining.stream()
).collect(Collectors.toList());
}
}).commandTimeout(Duration.ofMillis(timeout))
.poolConfig(genericObjectPoolConfig).build();
return new LettuceConnectionFactory(configuration, clientConfig);
複制
手動實作順序讀各個從節點,修改後調用情況如下,由于還有其他應用連接配接該redis,是以監控圖中非絕對均衡
哨兵模式
這個我就提供一個簡單demo
@Configuration
@ComponentScan("com.redis")
public class RedisConfig {
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
// return new LettuceConnectionFactory(new RedisStandaloneConfiguration("192.168.80.130", 6379));
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
.master("mymaster")
// 哨兵位址
.sentinel("192.168.80.130", 26379)
.sentinel("192.168.80.130", 26380)
.sentinel("192.168.80.130", 26381);
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder().
readFrom(ReadFrom.SLAVE_PREFERRED).build();
return new LettuceConnectionFactory(sentinelConfig, clientConfig);
}
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 可以配置對象的轉換規則,比如使用json格式對object進行存儲。
// Object --> 序列化 --> 二進制流 --> redis-server存儲
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
return redisTemplate;
}
}
複制
叢集模式
叢集模式就比較簡單了,直接套用下面demo
import io.lettuce.core.ReadFrom;
import io.lettuce.core.resource.ClientResources;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.HashSet;
import java.util.Set;
@Slf4j
@Configuration
public class Redis2Config {
@Value("${spring.redis2.cluster.nodes: com:9736}")
public String REDIS_HOST;
@Value("${spring.redis2.cluster.port:9736}")
public int REDIS_PORT;
@Value("${spring.redis2.cluster.type:}")
public String REDIS_TYPE;
@Value("${spring.redis2.cluster.read-from:master}")
public String READ_FROM;
@Value("${spring.redis2.cluster.max-redirects:1}")
public int REDIS_MAX_REDIRECTS;
@Value("${spring.redis2.cluster.share-native-connection:true}")
public boolean REDIS_SHARE_NATIVE_CONNECTION;
@Value("${spring.redis2.cluster.validate-connection:false}")
public boolean VALIDATE_CONNECTION;
@Value("${spring.redis2.cluster.shutdown-timeout:100}")
public long SHUTDOWN_TIMEOUT;
@Bean(value = "myRedisConnectionFactory")
public RedisConnectionFactory connectionFactory(ClientResources clientResources) {
RedisClusterConfiguration clusterConfiguration = new RedisClusterConfiguration();
if (StringUtils.isNotEmpty(REDIS_HOST)) {
String[] serverArray = REDIS_HOST.split(",");
Set<redisnode> nodes = new HashSet<redisnode>();
for (String ipPort : serverArray) {
String[] ipAndPort = ipPort.split(":");
nodes.add(new RedisNode(ipAndPort[0].trim(), Integer.valueOf(ipAndPort[1])));
}
clusterConfiguration.setClusterNodes(nodes);
}
if (REDIS_MAX_REDIRECTS > 0) {
clusterConfiguration.setMaxRedirects(REDIS_MAX_REDIRECTS);
}
LettucePoolingClientConfiguration.LettucePoolingClientConfigurationBuilder clientConfigurationBuilder = LettucePoolingClientConfiguration.builder()
.clientResources(clientResources).shutdownTimeout(Duration.ofMillis(SHUTDOWN_TIMEOUT));
if (READ_FROM.equals("slave")) {
clientConfigurationBuilder.readFrom(ReadFrom.SLAVE_PREFERRED);
} else if (READ_FROM.equals("nearest")) {
clientConfigurationBuilder.readFrom(ReadFrom.NEAREST);
} else if (READ_FROM.equals("master")) {
clientConfigurationBuilder.readFrom(ReadFrom.MASTER_PREFERRED);
}
LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(clusterConfiguration, clientConfigurationBuilder.build());
lettuceConnectionFactory.afterPropertiesSet();
return lettuceConnectionFactory;
}
@Bean(name = "myRedisTemplate")
public RedisTemplate myRedisTemplate(@Qualifier("myRedisConnectionFactory") RedisConnectionFactory connectionFactory) {
RedisTemplate template = new RedisTemplate();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
return template;
}
}
複制
不過這裡叢集模式不推薦讀取從節點,因為在生産中有可能導緻某一分片挂掉以至于整個叢集都不可用,可以考慮從節點整多個,然後配置讀寫分離。