上一篇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()構建出來的:
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的使用了,用過的自然看得懂上面的方法,有不懂的也可以留言提問哦。
好了,就到這吧,原創不易,怎麼支援你們知道,那麼下次見啦
關注公眾號獲取更多內容,有問題也可在公眾號提問哦:
強哥叨逼叨
叨逼叨程式設計、網際網路的見解和新鮮事