一、如何实现
具体使用方式及代码实现:https://gitee.com/mr_wenpan/basis-enhance/tree/master/enhance-boot-data-redis
1、实现流程
-
通过研究源码我们知道,spring-data-redis为我们提供的RedisTemplate默认是操作application.yml配置文件中的指定的redis db(如果没有指定则默认操作0号db)
-
如果要操作不同的db需要重置RedisTemplate中对于redis server的连接,频繁的重置连接是一笔很大的开销,而且很可能会造成安全性问题,所以不能通过重置redis db连接来实现
-
那么我们还可以通过什么方式实现呢?你提供的
RedisTemplate
不是默认操作application.yml配置文件中指定的redis db吗?那我们也可以根据你创建配置、创建连接工厂的方式自己创建连接到我们指定redis db的RedisTemplate
不就好了吗? -
每个
redisTemplate
对应着一个db,当你要动态切换db的时候,我们通过指定的db号动态的去获取对应RedisTemplate
,然后使用这个RedisTemplate
去操作db。 -
上面的流程很简单,复杂点在于基于源码的配置(构建连接工厂、构建连接配置、适配不同客户端)去做一些修改,将源码的配置修改成我们所需要的配置。
二、springboot自动配置中是怎么注入RedisTemplate的
先看springboot自动配置中是怎么注入RedisTemplate的,我们再模仿他注入
RedisTemplate
的方式产生我们自己的RedisTemplate
。
1、RedisAutoConfiguration自动注入配置类
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
// 导入了lettuce和jedis这两个配置类
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {// 可以看到如果要注入redisTemplate,需要容器中有RedisConnectionFactory的实现类(不能连接redis// 那么创建了RedisTemplate有屁用),那么RedisConnectionFactory是在哪里被注入的呢?@Bean@ConditionalOnMissingBean(name = "redisTemplate")@ConditionalOnSingleCandidate(RedisConnectionFactory.class)public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {RedisTemplate<Object, Object> template = new RedisTemplate<>();template.setConnectionFactory(redisConnectionFactory);return template;}}
2、RedisConnectionFactory注入流程
- 从上面第一步我们知道,在注入RedisTemplate的时候要求容器中必须有
RedisConnectionFactory
对象实例,那么RedisConnectionFactory
是从哪里来的?在哪里注入的?继续往下看 - 我们知道RedisConnectionFactory是一个接口,接口一般不能被注入的,但是他有两个实现类,分别是
LettuceConnectionFactory
和JedisConnectionFactory
,这两个连接工厂对应着创建和管理lettuce和jedis连接。 - 所以对于不同的redis客户端我们只需要注入不同的连接工厂即可。
- 以
LettuceConnectionFactory
为例看看LettuceConnectionFactory是在哪里被注入,怎样被注入的
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisClient.class)
// spring.redis.client-type 指定客户端类型
@ConditionalOnProperty(name = "spring.redis.client-type", havingValue = "lettuce", matchIfMissing = true)
class LettuceConnectionConfiguration extends RedisConnectionConfiguration {// 构造函数LettuceConnectionConfiguration(RedisProperties properties,ObjectProvider<RedisSentinelConfiguration> sentinelConfigurationProvider,ObjectProvider<RedisClusterConfiguration> clusterConfigurationProvider) {super(properties, sentinelConfigurationProvider, clusterConfigurationProvider);}// 创建RedisConnectionFactory并注入到容器@Bean@ConditionalOnMissingBean(RedisConnectionFactory.class)LettuceConnectionFactory redisConnectionFactory(ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers,ClientResources clientResources) {// 获取lettuce客户端配置(获取配置逻辑在下面)LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(builderCustomizers, clientResources,getProperties().getLettuce().getPool());// 通过lettuce客户端配置构建lettuce连接工厂,lettuce连接工厂是如何构建的,请看下面return createLettuceConnectionFactory(clientConfig);}// 创建lettuce连接工厂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);}}
3、构建客户端连接配置流程
- 由上面的第二步中可以看到,在注入
LettuceConnectionFactory
的时候需要构建lettuce连接配置LettuceConnectionConfiguration
,那么LettuceConnectionConfiguration
是在哪里产生的,继续往下看
// 构建lettuce客户端连接配置(该配置用于构建Redis连接工厂),可以看到,构建过程并不复杂
private LettuceClientConfiguration getLettuceClientConfiguration(ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers,ClientResources clientResources, Pool pool) {// lettuce客户端配置建造者,通过建造者去解析url、连接主机、端口等信息,然后构建成一个lettuce客户端配置LettuceClientConfigurationBuilder builder = createBuilder(pool);applyProperties(builder);if (StringUtils.hasText(getProperties().getUrl())) {customizeConfigurationFromUrl(builder);}builder.clientOptions(createClientOptions());builder.clientResources(clientResources);builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));return builder.build();
}
4、springboot自动配置注入RedisTemplate流程总结
- 在自动配置类中(
RedisAutoConfiguration
),首先通过配置文件 +@Bean
的方式new 了一个RedisTemplate
,但是在创建RedisTemplate的过程中需要用到RedisConnectionFactory
,所以下一步需要理解RedisConnectionFactory是如何注入的 RedisConnectionFactory
的注入逻辑在LettuceConnectionConfiguration
中可以看到,同样也是通过配置文件 +@Bean
的方式new 了一个LettuceConnectionFactory
,但是在创建LettuceConnectionFactory的时候需要用到一些连接配置信息,那么就需要去构建连接配置信息。- 在LettuceConnectionConfiguration中可以看到连接配置信息是使用建造者模式以及读取application.yml中相关redis配置信息来构建的redis连接配置
三、自己创建RedisTemplate
仿照springboot自动配置中注入RedisTemplate的逻辑自己创建对应不同redis db的
RedisTemplate
!
1、AbstractRoutingRedisTemplate
先创建一个类,该类直接继承RedisTemplate
,并且该类有个map属性,该map中以redis db作为key,以该db对应的RedisTemplate为value保存每个db对应的RedisTemplate,并且提供一个方法determineTargetRedisTemplate
通过指定的db号,从map中获取对应的RedisTemplate,代码如下:
public abstract class AbstractRoutingRedisTemplate<K, V> extends RedisTemplate<K, V> implements InitializingBean {/*** 存放对应库的redisTemplate,用于操作对应的db*/private Map<Object, RedisTemplate<K, V>> redisTemplates;/*** 当不指定库时默认使用的redisTemplate*/private RedisTemplate<K, V> defaultRedisTemplate;/*** 获取要操作的RedisTemplate*/protected RedisTemplate<K, V> determineTargetRedisTemplate() {// 当前要操作的DBObject lookupKey = determineCurrentLookupKey();// 如果当前要操作的DB为空则使用默认的RedisTemplate(使用0号库)if (lookupKey == null) {return defaultRedisTemplate;}RedisTemplate<K, V> redisTemplate = redisTemplates.get(lookupKey);// 如果当前要操作的db还没有维护到redisTemplates中,则创建一个对该库的连接并缓存起来if (redisTemplate == null) {redisTemplate = createRedisTemplateOnMissing(lookupKey);redisTemplates.put(lookupKey, redisTemplate);}return redisTemplate;}
}
2、DynamicRedisTemplate
上面已经提供了map来存放对应的db和RedisTemplate,所以这里提供一个子类去实现他的对应的抽象方法即可。
public class DynamicRedisTemplate<K, V> extends AbstractRoutingRedisTemplate<K, V> {/*** 动态RedisTemplate工厂,用于创建管理动态DynamicRedisTemplate*/private final DynamicRedisTemplateFactory<K, V> dynamicRedisTemplateFactory;public DynamicRedisTemplate(DynamicRedisTemplateFactory<K, V> dynamicRedisTemplateFactory) {this.dynamicRedisTemplateFactory = dynamicRedisTemplateFactory;}@Overrideprotected Object determineCurrentLookupKey() {return RedisDatabaseThreadLocalHelper.get();}/*** 通过制定的db创建RedisTemplate** @param lookupKey db号* @return org.springframework.data.redis.core.RedisTemplate<K, V>*/@Overrideprotected RedisTemplate<K, V> createRedisTemplateOnMissing(Object lookupKey) {return dynamicRedisTemplateFactory.createRedisTemplate((Integer) lookupKey);}}
3、DynamicRedisHelper
按照上面两步其实我们的功能就已经实现好了,可以直接注入容器使用了。但是使用上可能还不是特别方便。所以可以对DynamicRedisTemplate
再进行一层封装,将一些固化操作封装起来。
public class DynamicRedisHelper extends RedisHelper {private static final Logger logger = LoggerFactory.getLogger(DynamicRedisHelper.class);/*** 动态redisTemplate*/private final DynamicRedisTemplate<String, String> redisTemplate;public DynamicRedisHelper(DynamicRedisTemplate<String, String> redisTemplate) {this.redisTemplate = redisTemplate;}/*** 获取RedisTemplate对象*/@Overridepublic RedisTemplate<String, String> getRedisTemplate() {return redisTemplate;}/*** 更改当前线程 RedisTemplate database*/@Overridepublic void setCurrentDatabase(int database) {RedisDatabaseThreadLocalHelper.set(database);}// 清除当前线程的db@Overridepublic void clearCurrentDatabase() {RedisDatabaseThreadLocalHelper.clear();}
4、一些疑问点说明
①、对于RedisTemplate的注入流程通过源码阅读我们已经大概明白了,所以我们只需要仿照springboot自动配置中注入RedisTemplate的逻辑自己去注入RedisTemplate(自己构建连接工厂、lettuce或jedis配置信息等)!
②、为什么我们要自己构建Redis连接工厂和lettuce或jedis配置信息呢?不能用springboot自动配置源码中注入到容器中的RedisConnectionFactory
和LettuceConnectionConfiguration
去创建一个新的RedisTemplate吗?
-
从上面的源码中可以看到在创建redis连接配置信息的时候有三个方法(
getSentinelConfig
、getClusterConfiguration
、getStandaloneConfig
)需要去获取application.yml配置文件中的Redis配置,该方法源码如下protected final RedisStandaloneConfiguration getStandaloneConfig() {RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();if (StringUtils.hasText(this.properties.getUrl())) {// 解析Redis连接urlConnectionInfo connectionInfo = parseUrl(this.properties.getUrl());// 设置主机名config.setHostName(connectionInfo.getHostName());// 设置端口config.setPort(connectionInfo.getPort());// 设置用户名config.setUsername(connectionInfo.getUsername());// 设置密码config.setPassword(RedisPassword.of(connectionInfo.getPassword()));} else {config.setHostName(this.properties.getHost());config.setPort(this.properties.getPort());config.setUsername(this.properties.getUsername());config.setPassword(RedisPassword.of(this.properties.getPassword()));}// 设置所使用的 redis db 【关键点】// 设置db信息,这里的db是直接从配置文件中读取,如果我们还继续使用这个db的话,那么使用连接工厂创建的连接也是连接到这个// db上的,不会连接到我们指定的db上,达不到动态切换db的效果config.setDatabase(this.properties.getDatabase());return config; }
-
所以如果使用springboot自动配置注入到容器中的Redis连接工厂和redis连接配置去创建新的
RedisTemplate
,那么这个RedisTemplate操作的也是application.yml中指定的database,达不到动态切换redis db的效果
③、既然使用springboot自动配置注入到容器中的Redis连接工厂和redis连接配置去创建新的RedisTemplate
时只是所使用的redis db不一样,并且从源码中可以看到创建连接配置时所使用的redis db是application.yml
配置文件中指定的db,那么当我们自己注入RedisTemplate的时候我们,我们直接动态更改RedisProperties
中的database值就可以,那么这样不就解决了对于不同的redis db创建不同的RedisTemplate了吗?
- 确实,按照这个逻辑经过测试后确实可以实现,并且更加简单。但是考虑到这样动态更改
RedisProperties
中的database值,会不会对lettuce或jedis客户端产生隐藏bug ? - 即使当前版本的lettuce或jedis客户端只是在创建连接的时候才使用到了application.yml中的redis db,那么如果redis客户端有版本升级,升级后再源码中其他地方有使用到
RedisProperties
中的database,那么这种方式就会影响源码逻辑。 - 所以我们并不采用这种方式去实现,而是采用自己拷贝一份连接工厂、连接配置类,对某些必要的逻辑做一些改动和封装即可,这样也规避了版本升级带来的风险。