上一篇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的建立了呢?咱們來找找它被建立的地方:
走的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的使用了,用過的天然看得懂上面的方法,有不懂的也能夠留言提問哦。
好了,就到這吧,原創不易,怎麼支持大家知道,那麼下次見啦
關注公衆號獲取更多內容,有問題也可在公衆號提問哦:
強哥叨逼叨
叨逼叨編程、互聯網的看法和新鮮事