从源码研究如何不重启Springboot项目实现redis配置动态切换

上一篇Websocket的续篇暂时尚未动手写,这篇算是插播吧。今天讲讲不重启项目动态切换redis服务。redis

背景spring

多个项目或微服务场景下,各个项目都须要配置redis数据源。可是,每当运维搞事时(修改redis服务地址或端口),各个项目都须要进行重启才能链接上最新的redis配置。服务一多,修改各个项目配置而后重启项目就很是蛋疼。因此咱们想要找到一个可行的解决方案,可以不重启项目的状况下,修改配置,动态切换redis服务。编程

如何实现切换redis链接服务器

刚遇到这个问题的时候,想必若是对spring-boot-starter-data-redis不是很熟悉的人,首先想到的就是去百度一下(安慰下本身:不要重复造轮子嘛)。app

但是一阵百度以后,你找到的结果可能都是这样的:运维

public ValueOperations updateRedisConfig() {
    JedisConnectionFactory jedisConnectionFactory = (JedisConnectionFactory) stringRedisTemplate.getConnectionFactory();
    jedisConnectionFactory.setDatabase(db);
    stringRedisTemplate.setConnectionFactory(jedisConnectionFactory);
    ValueOperations valueOperations = stringRedisTemplate.opsForValue();
    return ValueOperations;

没错,绝大多数都是切换redis db的代码,而没有切redis服务地址或帐号密码的。并且天下代码一大抄,大多数博客都是同样的内容,这就让人很恶心。socket

没办法,网上没有,只能本身造轮子了。不过,从强哥这种懒人思惟来讲,上面的代码既然能切库,那是否是host、username、password也一样能够,因而咱们加入以下代码:spring-boot

public ValueOperations updateRedisConfig() {
    JedisConnectionFactory jedisConnectionFactory = (JedisConnectionFactory) stringRedisTemplate.getConnectionFactory();
    jedisConnectionFactory.setDatabase(db);
    jedisConnectionFactory.setHostName(host);
    jedisConnectionFactory.setPort(port);
    jedisConnectionFactory.setPassword(password);
    stringRedisTemplate.setConnectionFactory(jedisConnectionFactory);
    ValueOperations valueOperations = stringRedisTemplate.opsForValue();
    return valueOperations;
}

话很少说,改完重启一下。额,运行结果并无让咱们见证奇迹的时刻。在调用updateRedisConfig方法的以后,使用redisTemplate仍是只能切换db,不能进行服务地址或帐号密码的更新。微服务

这就让人头疼了,不过想也没错,若是能够的话,网上不该该找不到相似的代码。那么,如今该咋办嘞?工具

强哥的想法是:redisTemplate每次获取ValueOperations执行get/set方法的时候,都会去链接redis服务器,那么咱们就从这两个方法入手看看能不能找获得解决方案。

接下来就是源码研究的过程啦,有耐心的小伙伴就跟着强哥一块儿找,只想要结果的就跳到文末吧~

首先来看看入手工具方法set:

public boolean set(final String key, Object value) {
  boolean result = false;
  try {
          ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
          operations.set(key, value);
          result = true;
      } catch (Exception e) {
          logger.error("set cache error:", e);
      }
  return result;
}

咱们进入到operations.set(key, value);的set方法实现:

public boolean set(String key, Object value) {
        boolean result = false;
    try {
        ValueOperations<Serializable, Object> operations = this.redisTemplate.opsForValue();
        operations.set(key, value);
        result = true;
    } catch (Exception var5) {
      this.logger.error("set error:", var5);
    }
    return result;
}

哦,走的是execute方法,进去看看,具体调用的是AbstractOperations的RedisTemplate的execute方法(中间跳过几个重载方法跳转):

public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {
    Assert.isTrue(initialized, "template not initialized; call afterPropertiesSet() before using it");
    Assert.notNull(action, "Callback object must not be null");
    RedisConnectionFactory factory = getConnectionFactory();
    RedisConnection conn = null;
    try {
      if (enableTransactionSupport) {
// only bind resources in case of potential transaction synchronization
        conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);
      } else {
        conn = RedisConnectionUtils.getConnection(factory);
      }
      boolean existingConnection = TransactionSynchronizationManager.hasResource(factory);
      RedisConnection connToUse = preProcessConnection(conn, existingConnection);
      boolean pipelineStatus = connToUse.isPipelined();
      if (pipeline && !pipelineStatus) {
        connToUse.openPipeline();
      }
      RedisConnection connToExpose = (exposeConnection ? connToUse : createRedisConnectionProxy(connToUse));
      T result = action.doInRedis(connToExpose);
      // close pipeline
      if (pipeline && !pipelineStatus) {
        connToUse.closePipeline();
      }
      // TODO: any other connection processing?
      return postProcessResult(result, connToUse, existingConnection);
    } finally {
      RedisConnectionUtils.releaseConnection(conn, factory);
    }
}

方法内容很长,不过大体能够看出前面是获取一个RedisConnection对象,后面应该就是命令的执行,为何说应该?由于强哥也没去细看后面的实现,由于咱们要关注的就是怎么拿到这个RedisConnection对象的。

那么咱们走RedisConnectionUtils.getConnection(factory);这句代码进去看看,为何我知道是走这句而不是上面那句,由于强哥没开事务,若是你们有打断点,应该默认也是走的这句,跳到具体的实现方法:RedisConnectionUtils.doGetConnection(……):

public static RedisConnection doGetConnection(RedisConnectionFactory factory, boolean allowCreate, boolean bind,
boolean enableTransactionSupport) {
    Assert.notNull(factory, "No RedisConnectionFactory specified");
    RedisConnectionHolder connHolder = (RedisConnectionHolder) TransactionSynchronizationManager.getResource(factory);
    if (connHolder != null) {
      if (enableTransactionSupport) {
        potentiallyRegisterTransactionSynchronisation(connHolder, factory);
      }
      return connHolder.getConnection();
    }
    if (!allowCreate) {
      throw new IllegalArgumentException("No connection found and allowCreate = false");
    }
    if (log.isDebugEnabled()) {
      log.debug("Opening RedisConnection");
    }
    RedisConnection conn = factory.getConnection();
    if (bind) {
      RedisConnection connectionToBind = conn;
      if (enableTransactionSupport && isActualNonReadonlyTransactionActive()) {
        connectionToBind = createConnectionProxy(conn, factory);
      }
      connHolder = new RedisConnectionHolder(connectionToBind);
      TransactionSynchronizationManager.bindResource(factory, connHolder);
      if (enableTransactionSupport) {
        potentiallyRegisterTransactionSynchronisation(connHolder, factory);
      }
      return connHolder.getConnection();
    }
    return conn;
  }

代码仍是很长,话很少说,断点走的这句:RedisConnection conn = factory.getConnection();那就看看其实现方法吧:JedisConnectionFactory.getConnection(),这个是个关键方法:

public RedisConnection getConnection() {
 if (cluster != null) {
   return getClusterConnection();
 }
 Jedis jedis = fetchJedisConnector();
 JedisConnection connection = (usePool ? new JedisConnection(jedis, pool, dbIndex, clientName)
     : new JedisConnection(jedis, null, dbIndex, clientName));
 connection.setConvertPipelineAndTxResults(convertPipelineAndTxResults);
 return postProcessConnection(connection);
}

看到了,代码很短,可是咱们从中能够获取到的内容却不少:

第一个判断是是否有集群,这个强哥项目暂时没用,因此无论;若是你们有用到,可能要要考虑下里面的代码。

Jedis对象是在这里建立的,熟悉redis的应该都知道:Jedis是Redis官方推荐的Java链接开发工具。直接用它就能执行redis命令。

usePool 这个变量,说明咱们链接的redis服务器的时候可能用到了链接池;不知道你们看到usePool会不会有种恍然醒悟的感受,极可能就是由于咱们使用了链接池,因此即便咱们以前的代码中切换了帐号密码,链接池的链接仍是没有更新致使的处理无效。

咱们先看看fetchJedisConnector方法实现:

protected Jedis fetchJedisConnector() {
  try {
    if (usePool && pool != null) {
      return pool.getResource();
    }
 
    Jedis jedis = new Jedis(getShardInfo());
  // force initialization (see Jedis issue #82)
    jedis.connect();
  
    potentiallySetClientName(jedis);
    return jedis;
  } catch (Exception ex) {
throw new RedisConnectionFailureException("Cannot get Jedis connection", ex);
  }
}

哦,能够看到,Jedis对象是根据getShardInfo()构建出来的:

public BinaryJedis(JedisShardInfo shardInfo) {
  this.client = new Client(shardInfo.getHost(), shardInfo.getPort(), shardInfo.getSsl(), shardInfo.getSslSocketFactory(), shardInfo.getSslParameters(), shardInfo.getHostnameVerifier());
  this.client.setConnectionTimeout(shardInfo.getConnectionTimeout());
  this.client.setSoTimeout(shardInfo.getSoTimeout());
  this.client.setPassword(shardInfo.getPassword());
  this.client.setDb((long)shardInfo.getDb());
}

那就是说,只要咱们掌握了这个JedisShardInfo的由来,咱们就能够实现redis相关配置的切换。而这个getShardInfo()方法就是返回了JedisConnetcionFactory类的JedisShardInfo shardInfo属性:

public JedisShardInfo getShardInfo() {
  return shardInfo;
}

那么若是咱们知道了这个shardInfo是如何建立的,是否是就能够干预到RedisConnect的建立了呢?咱们来找找它被建立的地方:
image.png
走的JedisConnectionFactory的afterPropertiesSet()进去看看:

/*
  * (non-Javadoc)
  * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
  */
public void afterPropertiesSet() {
 if (shardInfo == null) {
   shardInfo = new JedisShardInfo(hostName, port);
   if (StringUtils.hasLength(password)) {
     shardInfo.setPassword(password);
   }  
   if (timeout > 0) {
       setTimeoutOn(shardInfo, timeout);
     }
   }

   if (usePool && clusterConfig == null) {
     this.pool = createPool();
   }
 
   if (clusterConfig != null) {
     this.cluster = createCluster();
   }
}

哦吼~,整篇博文最关键的代码终于出现了。咱们能够看到,JedisShardInfo的全部信息都是从JedisConnetionFactory的属性中来的,包括hostName、port、password、timeout等。并且,若是JedisShardInfo为null时,调用afterPropertiesSet方法会帮咱们建立出来。而后,该方法还会帮咱们建立新的链接池,简直完美。最最重要的是,这个方法是public的。

因此,嘿嘿,综上,咱们总结改造的几个点:

1.链接redis用到了链接池,须要先给他销毁;

2.建立Jedis的时候,将JedisShardInfo先设为null;

3.手动设置JedisConnetionFactory的hostName、port、password等信息;

4.调用JedisConnetionFactory的afterPropertiesSet方法建立JedisShardInfo;

5.给RedisTemplate设置处理后的JedisConnetionFactory,这样在下次使用set或get方法的时候就会去建立新改配置的链接池啦。

实现以下:

public void updateRedisConfig() {
  RedisTemplate template = (RedisTemplate) applicationContext.getBean("redisTemplate");
  JedisConnectionFactory redisConnectionFactory = (JedisConnectionFactory) template.getConnectionFactory();
//关闭链接池
  redisConnectionFactory.destroy();
  redisConnectionFactory.setShardInfo(null);
  redisConnectionFactory.setHostName(host);
  redisConnectionFactory.setPort(port);
  redisConnectionFactory.setPassword(password);
  redisConnectionFactory.setDatabase(database);
  //从新建立链接池
  redisConnectionFactory.afterPropertiesSet();
  template.setConnectionFactory(redisConnectionFactory);
}

重启项目以后,调用这个方法,就能够实现redis库及服务地址、帐号密码的切换而无需重启项目了。

如何实现动态切换

强哥这里就使用同一配置中心Apollo来进行动态配置的。

首先不懂Apollo是什么的同窗,先Apollo官网半日游吧(直接看官网教程,比看其余博客强)。简单的说就是一个统一配置中心,将原来配置在项目本地的配置(如:Spring中的application.properties)迁移到Apollo上,实现统一的管理。

使用Apollo的缘由,其实就是由于其接入简单,且具备实时更新回调的功能,咱们能够监听Apollo上的配置修改,实现针对修改的配置内容进行相应的回调监听处理。

所以咱们能够将redis的配置信息配置在Apollo上,而后监听这些配置。当Apollo上的这些配置修改时,咱们在ConfigChangeListener中,调用上面的updateRedisConfig方法就能够实现redis配置的动态切换了。

接入Apollo代码很是简单:

Config redisConfig = ConfigService.getConfig("redis");
ConfigChangeListener listener = this::updateRedisConfig;
redisConfig.addChangeListener(listener);

这样,咱们就能够实现具体所谓的动态更新配置啦~

固然,其余有相同功能的配置中心其实也能够,只是强哥项目中暂时用的就是Apollo就拿Apollo来说了。

考虑到篇幅已经很长了,就很少解释Apollo的使用了,用过的天然看得懂上面的方法,有不懂的也能够留言提问哦。

好了,就到这吧,原创不易,怎么支持大家知道,那么下次见啦

关注公众号获取更多内容,有问题也可在公众号提问哦:
image

强哥叨逼叨

叨逼叨编程、互联网的看法和新鲜事

相关文章
相关标签/搜索