java实现redis动态切换db
Posted wen-pan
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java实现redis动态切换db相关的知识,希望对你有一定的参考价值。
一、如何实现
具体使用方式及代码实现: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() {
// 当前要操作的DB
Object 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;
}
@Override
protected Object determineCurrentLookupKey() {
return RedisDatabaseThreadLocalHelper.get();
}
/**
* 通过制定的db创建RedisTemplate
*
* @param lookupKey db号
* @return org.springframework.data.redis.core.RedisTemplate<K, V>
*/
@Override
protected 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对象
*/
@Override
public RedisTemplate<String, String> getRedisTemplate() {
return redisTemplate;
}
/**
* 更改当前线程 RedisTemplate database
*/
@Override
public void setCurrentDatabase(int database) {
RedisDatabaseThreadLocalHelper.set(database);
}
// 清除当前线程的db
@Override
public 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连接url ConnectionInfo 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,那么这种方式就会影响源码逻辑。 - 所以我们并不采用这种方式去实现,而是采用自己拷贝一份连接工厂、连接配置类,对某些必要的逻辑做一些改动和封装即可,这样也规避了版本升级带来的风险。
以上是关于java实现redis动态切换db的主要内容,如果未能解决你的问题,请参考以下文章