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

简介:

从源码研究如何不重启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 execute(RedisCallback 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的使用了,用过的自然看得懂上面的方法,有不懂的也可以留言提问哦。

原文地址https://www.cnblogs.com/breakingdawn/p/13043921.html

相关文章
|
NoSQL Java 关系型数据库
微服务——SpringBoot使用归纳——Spring Boot 中集成Redis——Redis 介绍
本文介绍在 Spring Boot 中集成 Redis 的方法。Redis 是一种支持多种数据结构的非关系型数据库(NoSQL),具备高并发、高性能和灵活扩展的特点,适用于缓存、实时数据分析等场景。其数据以键值对形式存储,支持字符串、哈希、列表、集合等类型。通过将 Redis 与 Mysql 集群结合使用,可实现数据同步,提升系统稳定性。例如,在网站架构中优先从 Redis 获取数据,故障时回退至 Mysql,确保服务不中断。
581 0
微服务——SpringBoot使用归纳——Spring Boot 中集成Redis——Redis 介绍
|
12月前
|
NoSQL Java 网络安全
SpringBoot启动时连接Redis报错:ERR This instance has cluster support disabled - 如何解决?
通过以上步骤一般可以解决由于配置不匹配造成的连接错误。在调试问题时,一定要确保服务端和客户端的Redis配置保持同步一致。这能够确保SpringBoot应用顺利连接到正确配置的Redis服务,无论是单机模式还是集群模式。
847 5
|
NoSQL Java 调度
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
分布式锁是分布式系统中用于同步多节点访问共享资源的机制,防止并发操作带来的冲突。本文介绍了基于Spring Boot和Redis实现分布式锁的技术方案,涵盖锁的获取与释放、Redis配置、服务调度及多实例运行等内容,通过Docker Compose搭建环境,验证了锁的有效性与互斥特性。
1020 0
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
|
机器学习/深度学习 数据采集 人机交互
springboot+redis互联网医院智能导诊系统源码,基于医疗大模型、知识图谱、人机交互方式实现
智能导诊系统基于医疗大模型、知识图谱与人机交互技术,解决患者“知症不知病”“挂错号”等问题。通过多模态交互(语音、文字、图片等)收集病情信息,结合医学知识图谱和深度推理,实现精准的科室推荐和分级诊疗引导。系统支持基于规则模板和数据模型两种开发原理:前者依赖人工设定症状-科室规则,后者通过机器学习或深度学习分析问诊数据。其特点包括快速病情收集、智能病症关联推理、最佳就医推荐、分级导流以及与院内平台联动,提升患者就诊效率和服务体验。技术架构采用 SpringBoot+Redis+MyBatis Plus+MySQL+RocketMQ,确保高效稳定运行。
1050 0
|
存储 人工智能 NoSQL
SpringBoot整合Redis、ApacheSolr和SpringSession
本文介绍了如何使用SpringBoot整合Redis、ApacheSolr和SpringSession。SpringBoot以其便捷的配置方式受到开发者青睐,通过引入对应的starter依赖,可轻松实现功能整合。对于Redis,可通过配置RedisSentinel实现高可用;SpringSession则提供集群Session管理,支持多种存储方式如Redis;整合ApacheSolr时,借助Zookeeper搭建SolrCloud提高可用性。文中详细说明了各组件的配置步骤与代码示例,方便开发者快速上手。
306 11
|
NoSQL Java API
微服务——SpringBoot使用归纳——Spring Boot 中集成Redis——Spring Boot 集成 Redis
本文介绍了在Spring Boot中集成Redis的方法,包括依赖导入、Redis配置及常用API的使用。通过导入`spring-boot-starter-data-redis`依赖和配置`application.yml`文件,可轻松实现Redis集成。文中详细讲解了StringRedisTemplate的使用,适用于字符串操作,并结合FastJSON将实体类转换为JSON存储。还展示了Redis的string、hash和list类型的操作示例。最后总结了Redis在缓存和高并发场景中的应用价值,并提供课程源代码下载链接。
2928 0
|
NoSQL Java Redis
微服务——SpringBoot使用归纳——Spring Boot 中集成Redis——Redis 安装
本教程介绍在 VMware 虚拟机(CentOS 7)或阿里云服务器中安装 Redis 的过程,包括安装 gcc 编译环境、下载 Redis(官网或 wget)、解压安装、修改配置文件(如 bind、daemonize、requirepass 等设置)、启动 Redis 服务及测试客户端连接。通过 set 和 get 命令验证安装是否成功。适用于初学者快速上手 Redis 部署。
586 0
|
缓存 NoSQL 关系型数据库
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
|
缓存 NoSQL Java
Redis+Caffeine构建高性能二级缓存
大家好,我是摘星。今天为大家带来的是Redis+Caffeine构建高性能二级缓存,废话不多说直接开始~
1928 0
|
缓存 负载均衡 监控
135_负载均衡:Redis缓存 - 提高缓存命中率的配置与最佳实践
在现代大型语言模型(LLM)部署架构中,缓存系统扮演着至关重要的角色。随着LLM应用规模的不断扩大和用户需求的持续增长,如何构建高效、可靠的缓存架构成为系统性能优化的核心挑战。Redis作为业界领先的内存数据库,因其高性能、丰富的数据结构和灵活的配置选项,已成为LLM部署中首选的缓存解决方案。
1063 25