一、简介
当你使用 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 同时包含
- spring.redis.host 和 spring.redis.port;
- spring.redis.sentinel.master 和 spring.redis.sentinel.nodes;
- 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 模式的方法:
- 配置文件:可以通过 application.properties 中的属性来设置哨兵模式,集群模式,单机模式;
- Java代码:可以通过向 Spring 注入Bean的模式来设置哨兵模式(注入 RedisSentinelConfiguration)和集群模式(注入 RedisClusterConfiguration);
Java代码优先级 > 配置文件;
另外,我们还知道如果同时存在多种模式的配置时,最终只会选取一种模式,此时就要根据优先级来判断具体选择哪一种:
哨兵模式 > 集群模式 > 单机模式
最后,就是 spring.redis.url 这个属性,对于配置 Lettuce 作为 Redis 客户端时,没啥卵用。