天天看點

redis讀寫分離之lettuce問題讀寫分離

問題

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時每次都會擷取最後一個,下面是緩存指令情況
redis讀寫分離之lettuce問題讀寫分離

解決方案就是自定義一個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 -&gt; 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,是以監控圖中非絕對均衡

redis讀寫分離之lettuce問題讀寫分離

哨兵模式

這個我就提供一個簡單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 --&gt; 序列化 --&gt; 二進制流 --&gt; 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 &gt; 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;
    }


}           

複制

不過這裡叢集模式不推薦讀取從節點,因為在生産中有可能導緻某一分片挂掉以至于整個叢集都不可用,可以考慮從節點整多個,然後配置讀寫分離。