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

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

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

强哥叨逼叨

叨逼叨编程、互联网的见解和新鲜事

从源码研究如何不重启Springboot项目实现redis配置动态切换的更多相关文章

  1. springboot脚手架liugh-parent源码研究参考

    1. liugh-parent源码研究参考 1.1. 前言 这也是个开源的springboot脚手架项目,这里研究记录一些该框架写的比较好的代码段和功能 脚手架地址 1.2. 功能 1.2.1. 当前 ...

  2. OAuth2学习及DotNetOpenAuth部分源码研究

    OAuth2学习及DotNetOpenAuth部分源码研究 在上篇文章中我研究了OpenId及DotNetOpenAuth的相关应用,这一篇继续研究OAuth2. 一.什么是OAuth2 OAuth是 ...

  3. Android开源项目 Universal imageloader 源码研究之Lru算法

    https://github.com/nostra13/Android-Universal-Image-Loader universal imageloader 源码研究之Lru算法 LRU - Le ...

  4. zepto源码研究 - zepto.js - 1

    简要:网上已经有很多人已经将zepto的源码研究得很细致了,但我还是想写下zepto源码系列,将别人的东西和自己的想法写下来以加深印象也是自娱自乐,文章中可能有许多错误,望有人不吝指出,烦请赐教. 首 ...

  5. dubbo源码研究(一)

    1. dubbo源码研究(一) 1.1. dubbo启动加载过程 我们知道,现在流行注解方式,用spring管理服务,dubbo最常用的就是@Reference和@Service了,那么我首先找到这两 ...

  6. 【JavaScript】$.extend使用心得及源码研究

    最近写多了js的面向对象编程,用$.extend写继承写得很顺手.但是在使用过程中发现有几个问题. 1.深拷贝 $.extend默认是浅拷贝,这意味着在继承复杂对象时,对象中内嵌的对象无法被拷贝到. ...

  7. underscore.js源码研究(8)

    概述 很早就想研究underscore源码了,虽然underscore.js这个库有些过时了,但是我还是想学习一下库的架构,函数式编程以及常用方法的编写这些方面的内容,又恰好没什么其它要研究的了,所以 ...

  8. underscore.js源码研究(7)

    概述 很早就想研究underscore源码了,虽然underscore.js这个库有些过时了,但是我还是想学习一下库的架构,函数式编程以及常用方法的编写这些方面的内容,又恰好没什么其它要研究的了,所以 ...

  9. underscore.js源码研究(6)

    概述 很早就想研究underscore源码了,虽然underscore.js这个库有些过时了,但是我还是想学习一下库的架构,函数式编程以及常用方法的编写这些方面的内容,又恰好没什么其它要研究的了,所以 ...

随机推荐

  1. app测试、web测试-怎么测?

    app测试 前言 看过许多大神对APP测试的理解,博主总结了一下我们平时测试APP应该注意的一些测试点并结合大神的理解,总结出这篇文章. 一.测试周期 测试周期一般为两周,根据项目情况以及版本质量可适 ...

  2. RobotFramework自动化测试之元素定位

    前言:最近在做基于RF框架的Web自动化测试,其中涉及到元素的定位,主要用到id.name.xpath.css四中定位方法,尤其后面的两种方法特别有效,可以解决大部分的定位问题. id和name定位 ...

  3. mysql 5.7.22安装

    1.解压目录. 2.安装 3.重新设置密码 set password=password('123456'); FLUSH PRIVILEGES;

  4. 4.4 Go goto continue break

    4.4 Go goto continue break Go语言的goto语句可以无条件的跳转到指定的代码行执行. goto语句一般与条件语句结合,实现条件转义,跳出循环体等. Go程序不推荐使用got ...

  5. oracle关于rownum的使用【oracle】

    转自:https://blog.csdn.net/qiuzhi__ke/article/details/78892822 关于rownum是怎么产生的(网上有不少的文章,下面是摘录): rownum是 ...

  6. 存储系列之 Linux ext2 概述

    引言:学习经典永不过时. 我们之前介绍过存储介质主要是磁盘,先介绍过物理的,后又介绍了虚拟的.保存在磁盘上的信息一般采用文件(file)为单位,磁盘上的文件必须是持久的,同时文件是通过操作系统管理的, ...

  7. PAT-1080 Graduate Admission (结构体排序)

    1080. Graduate Admission It is said that in 2013, there were about 100 graduate schools ready to pro ...

  8. C语言基础知识(四)——位操作

    一.进制基础知识 1.通常,1字节(Byte)包含8位(bit).C语言用字节表示储存系统字符集所需的大小. 2.对于一个1字节8位的二进制数,最右边(第0位)是最低阶位,最左边(第1位)是最高阶位, ...

  9. Java——去除字符串中的中文

    import java.util.regex.Matcher; import java.util.regex.Pattern; public class RemoveStrChinese { priv ...

  10. vue移动端转场动画

    vue移动端转场动画 1.介绍:使用vue移动端做项目的时候,为了用户的体验良好,我们需要页面有一种进入和转出的效果 // 在App.vue根组件中 <template> <div ...