一、如何实现

具体使用方式及代码实现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。

  • 上面的流程很简单,复杂点在于基于源码的配置(构建连接工厂、构建连接配置、适配不同客户端)去做一些修改,将源码的配置修改成我们所需要的配置。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TvVFCAuZ-1628749647181)(picture/动态切换db实现流程图.png)]

二、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是一个接口,接口一般不能被注入的,但是他有两个实现类,分别是LettuceConnectionFactoryJedisConnectionFactory,这两个连接工厂对应着创建和管理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流程总结
  1. 在自动配置类中(RedisAutoConfiguration),首先通过配置文件 + @Bean的方式new 了一个RedisTemplate,但是在创建RedisTemplate的过程中需要用到RedisConnectionFactory,所以下一步需要理解RedisConnectionFactory是如何注入的
  2. RedisConnectionFactory的注入逻辑在LettuceConnectionConfiguration 中可以看到,同样也是通过配置文件 + @Bean的方式new 了一个LettuceConnectionFactory,但是在创建LettuceConnectionFactory的时候需要用到一些连接配置信息,那么就需要去构建连接配置信息。
  3. 在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自动配置源码中注入到容器中的RedisConnectionFactoryLettuceConnectionConfiguration去创建一个新的RedisTemplate吗?

  • 从上面的源码中可以看到在创建redis连接配置信息的时候有三个方法(getSentinelConfiggetClusterConfigurationgetStandaloneConfig)需要去获取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,那么这种方式就会影响源码逻辑。
  • 所以我们并不采用这种方式去实现,而是采用自己拷贝一份连接工厂、连接配置类,对某些必要的逻辑做一些改动和封装即可,这样也规避了版本升级带来的风险。
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐