• LettuceConnectionConfiguration源码解析


    一、简介

    当你使用 SpringBoot 框架时,自动装配的功能很方便,比如你引用 redis 的依赖:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    

    当你不考虑整合 jedis 时,默认使用的是 lettuce。

    简单说明一下 LettuceConnectionConfiguration 是什么时候自动装配的:

    spring-boot-autoconfigure 有一个 RedisAutoConfiguration

    ...
    @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
    public class RedisAutoConfiguration {
      ...
    }
    

    之后会去创建 @Import 指明的 Bean,首先就是 LettuceConnectionConfiguration。

    二、构造函数

    // 第一次参数来自于 application.properties 中以 spring.redis 为前缀的属性
    // 第二个参数来自于我们注入Spring容器的RedisSentinelConfiguration实例
    // 第三个参数同理,来自于我们注入Spring容器的RedisClusterConfiguration实例
    LettuceConnectionConfiguration(RedisProperties properties,
      ObjectProvider<RedisSentinelConfiguration> sentinelConfigurationProvider,
      ObjectProvider<RedisClusterConfiguration> clusterConfigurationProvider) {
        super(properties, sentinelConfigurationProvider, clusterConfigurationProvider);
    }
    

    比较特别的就是参数 ObjectProvider,可以阅读这篇 Spring ObjectProvider使用说明 了解。

    • 如果注入实例为空时,使用ObjectProvider则避免了强依赖导致的依赖对象不存在异常;
    • 如果有多个实例,ObjectProvider的方法可以根据Bean实现的Ordered接口或@Order注解指定的先后顺序获取一个Bean。

    ObjectProvider 为Spring用户提供了一个更加宽松的依赖注入方式。

    三、注入RedisConnectionFactory

    // 如果Spring容器中还没有 RedisConnectionFactory 的实例,则向容器中注入LettuceConnectionFactory
    @Bean
    @ConditionalOnMissingBean(RedisConnectionFactory.class)
    LettuceConnectionFactory redisConnectionFactory(
      ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers,
      ClientResources clientResources) {
      // 获取 Lettuce 客户端配置
      LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(builderCustomizers, clientResources, getProperties().getLettuce().getPool());
      return createLettuceConnectionFactory(clientConfig);
    }
    

    连接工厂实例的优先级:

    // 从加载优先级来看,代码设计的逻辑是 哨兵模式 > 集群模式 > 单机模式
    private LettuceConnectionFactory createLettuceConnectionFactory(LettuceClientConfiguration clientConfiguration) {
      if (getSentinelConfig() != null) {
        // 创建哨兵模式对应的连接工厂
        return new LettuceConnectionFactory(getSentinelConfig(), clientConfiguration);
      }
      if (getClusterConfiguration() != null) {
        // 创建集群模式对应的连接工厂
        return new LettuceConnectionFactory(getClusterConfiguration(), clientConfiguration);
      }
      // 创建单机模式对应的连接工厂
      return new LettuceConnectionFactory(getStandaloneConfig(), clientConfiguration);
    }
    

    比方说,你在 application.properties 同时包含

    1. spring.redis.host 和 spring.redis.port;
    2. spring.redis.sentinel.master 和 spring.redis.sentinel.nodes;
    3. spring.redis.cluster.nodes 和 spring.redis.cluster.maxRedirects;

    这样三组配置同时存在时,最终会采用第二种哨兵模式,而忽略第一种单机模式以及第三种集群模式对应的配置。

    四、getSentinelConfig

    如果服务器为哨兵模式,客户端对应哨兵模式的配置:

    protected final RedisSentinelConfiguration getSentinelConfig() {
      // 如果这个不为空,则说明Spring容器中有RedisSentinelConfiguration类型的Bean
      // 同时说明,从优先级来看,Java代码注入的RedisSentinelConfiguration类型的Bean > application.properties 中以 spring.redis.sentinel 为前缀的配置
      if (this.sentinelConfiguration != null) {
        return this.sentinelConfiguration;
      }
      RedisProperties.Sentinel sentinelProperties = this.properties.getSentinel();
      if (sentinelProperties != null) {
        RedisSentinelConfiguration config = new RedisSentinelConfiguration();
        // 哨兵服务器可以监控多组 master-slave,这里指定连接其中某组 master-slave 的名字
        // 例如,sentinel.conf 中的配置 sentinel monitor mymaster 172.22.0.3 6379 2
        // mymaster就是我们需要的值
        config.master(sentinelProperties.getMaster());
        // 哨兵服务器的 ip:port 解析成 RedisNode
        config.setSentinels(createSentinels(sentinelProperties));
        config.setUsername(this.properties.getUsername());
        // 如果 redis-server 配置了 requirepass 属性,则客户端需要提供密码
        if (this.properties.getPassword() != null) {
          config.setPassword(RedisPassword.of(this.properties.getPassword()));
        }
        // 如果 redis-sentinel 配置了 requirepass 属性,则客户端需要提供密码
        if (sentinelProperties.getPassword() != null) {
          config.setSentinelPassword(RedisPassword.of(sentinelProperties.getPassword()));
        }
        config.setDatabase(this.properties.getDatabase());
        return config;
      }
      return null;
    }
    

    五、getClusterConfiguration

    如果服务器为集群模式,客户端对应集群模式的配置:

    protected final RedisClusterConfiguration getClusterConfiguration() {
      // 如果这个不为空,则说明Spring容器中有RedisClusterConfiguration类型的Bean
      // 同时说明,从优先级来看,Java代码注入的RedisClusterConfiguration类型的Bean > application.properties 中以 spring.redis.cluster 前缀的配置
      if (this.clusterConfiguration != null) {
        return this.clusterConfiguration;
      }
      if (this.properties.getCluster() == null) {
        return null;
      }
      RedisProperties.Cluster clusterProperties = this.properties.getCluster();
      // Redis 集群节点配置,形式为 ip:port,多个节点之间用逗号分隔
      RedisClusterConfiguration config = new RedisClusterConfiguration(clusterProperties.getNodes());
      if (clusterProperties.getMaxRedirects() != null) {
        config.setMaxRedirects(clusterProperties.getMaxRedirects());
      }
      config.setUsername(this.properties.getUsername());
      // Redis 集群节点设置了密码,则客户端需要提供密码
      if (this.properties.getPassword() != null) {
        config.setPassword(RedisPassword.of(this.properties.getPassword()));
      }
      return config;
    }
    

    六、parseUrl

    这个 parseUrl 其实是属于 LettuceConnectionConfiguration 的父类 RedisConnectionConfiguration

    // 合法的参数,形如字符串  redis://user:password@example.com:6379
    protected ConnectionInfo parseUrl(String url) {
      try {
        URI uri = new URI(url);
        // 协议名,以 redis:// 或者 rediss:// 开头
        String scheme = uri.getScheme();
        if (!"redis".equals(scheme) && !"rediss".equals(scheme)) {
          throw new RedisUrlSyntaxException(url);
        }
        如果是 rediss ,则表示使用 SSL 安全协议
        boolean useSsl = ("rediss".equals(scheme));
        String username = null;
        String password = null;
        // 指的是url中双斜杠之后,@之前的内容
        if (uri.getUserInfo() != null) {
          String candidate = uri.getUserInfo();
          int index = candidate.indexOf(':');
          if (index >= 0) {
            // 如果是 username:pwd 的形式
            username = candidate.substring(0, index);
            password = candidate.substring(index + 1);
          }
          else {
            // 如果是 username 的形式
            password = candidate;
          }
        }
        return new ConnectionInfo(uri, useSsl, username, password);
      }
      catch (URISyntaxException ex) {
        throw new RedisUrlSyntaxException(url, ex);
      }
    }
    

    我不过,因为接下来的这段代码,所以我个人判定 spring.redis.url 用处并不大,只能影响 useSsl 这一个属性:

    private void customizeConfigurationFromUrl(LettuceClientConfiguration.LettuceClientConfigurationBuilder builder) {
      ConnectionInfo connectionInfo = parseUrl(getProperties().getUrl());
      // 解析出来的 username,password 完全没用上
      if (connectionInfo.isUseSsl()) {
        builder.useSsl();
      }
    }
    

    七、总结

    通过阅读 LettuceConnectionConfiguration 的源码,我们知道两种在客户端代码中配置 redis-server 模式的方法:

    1. 配置文件:可以通过 application.properties 中的属性来设置哨兵模式,集群模式,单机模式;
    2. Java代码:可以通过向 Spring 注入Bean的模式来设置哨兵模式(注入 RedisSentinelConfiguration)和集群模式(注入 RedisClusterConfiguration);

    Java代码优先级 > 配置文件;

    另外,我们还知道如果同时存在多种模式的配置时,最终只会选取一种模式,此时就要根据优先级来判断具体选择哪一种:

    哨兵模式 > 集群模式 > 单机模式

    最后,就是 spring.redis.url 这个属性,对于配置 Lettuce 作为 Redis 客户端时,没啥卵用。

  • 相关阅读:
    代码改变世界,随手写了点代码解决了一个小学生级别的作业题,编程要从娃娃抓起
    智和信通实现信创国产化适配 助力信创生态智能运维体系建设
    智和信通赋能国产信创 建设IT智能监控运维体系
    北京智和信通智慧档案馆网络监控运维解决方案
    图书馆网络运维监控安全态势感知方案-智和信通
    广播电视网络运维安全态势感知解决方案
    北京智和信通荣膺2020智能运维星耀榜最具影响力企业
    直播新规出台!如何用技术管住乱打赏的「熊孩子」?
    是什么让你在赛博空间更好看?
    Niu Talk 数据科学系列论坛:明晚,我们聊聊大数据与开源
  • 原文地址:https://www.cnblogs.com/kendoziyu/p/15825835.html
Copyright © 2020-2023  润新知