從源碼研究如何不重啟Springboot項目實現redis配置動態切換_包裝設計

※產品缺大量曝光嗎?你需要的是一流包裝設計!

窩窩觸角包含自媒體、自有平台及其他國家營銷業務等,多角化經營並具有國際觀的永續理念。

上一篇Websocket的續篇暫時還沒有動手寫,這篇算是插播吧。今天講講不重啟項目動態切換redis服務。

背景

多個項目或微服務場景下,各個項目都需要配置redis數據源。但是,每當運維搞事時(修改redis服務地址或端口),各個項目都需要進行重啟才能連接上最新的redis配置。服務一多,修改各個項目配置然後重啟項目就非常蛋疼。所以我們想要找到一個可行的解決方案,能夠不重啟項目的情況下,修改配置,動態切換redis服務。

如何實現切換redis連接

剛遇到這個問題的時候,想必如果對spring-boot-starter-data-redis不是很熟悉的人,首先想到的就是去百度一下(安慰下自己:不要重複造輪子嘛)。

可是一陣百度之後,你找到的結果可能都是這樣的:

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

沒錯,絕大多數都是切換redis db的代碼,而沒有切redis服務地址或賬號密碼的。而且天下代碼一大抄,大多數博客都是一樣的內容,這就讓人很噁心。

沒辦法,網上沒有,只能自己造輪子了。不過,從強哥這種懶人思維來說,上面的代碼既然能切庫,那是不是host、username、password也同樣可以,於是我們加入如下代碼:

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()構建出來的:

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網動廣告出品的網頁設計,採用精簡與質感的CSS語法,提升企業的專業形象與簡約舒適的瀏覽體驗,讓瀏覽者第一眼就愛上她。

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的創建了呢?我們來找找它被創建的地方:

走的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的使用了,用過的自然看得懂上面的方法,有不懂的也可以留言提問哦。

好了,就到這吧,原創不易,怎麼支持你們知道,那麼下次見啦

關注公眾號獲取更多內容,有問題也可在公眾號提問哦:

強哥叨逼叨

叨逼叨編程、互聯網的見解和新鮮事

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

上新台中搬家公司提供您一套專業有效率且人性化的辦公室搬遷、公司行號搬家及工廠遷廠的搬家服務