LettuceConnectionConfiguration源码解析


一、简介

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


  org.springframework.boot
  spring-boot-starter-data-redis

当你不考虑整合 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 sentinelConfigurationProvider,
  ObjectProvider 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 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 客户端时,没啥卵用。