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

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 上一篇Websocket的续篇暂时还没有动手写,这篇算是插播吧。今天讲讲不重启项目动态切换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()构建出来的:


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的创建了呢?我们来找找它被创建的地方:


33.png


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


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

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
5天前
|
存储 SQL 关系型数据库
2024Mysql And Redis基础与进阶操作系列(1)作者——LJS[含MySQL的下载、安装、配置详解步骤及报错对应解决方法]
Mysql And Redis基础与进阶操作系列(1)之[MySQL的下载、安装、配置详解步骤及报错对应解决方法]
|
1月前
|
NoSQL Linux Redis
在 centos7 下重启/开启 redis 服务器
本文提供了一种在Centos 7操作系统下如何重启Redis服务器的步骤,包括停止Redis服务、确认停止成功以及重新启动Redis服务。
73 2
在 centos7 下重启/开启 redis 服务器
|
17天前
|
存储 NoSQL Redis
Redis 配置
10月更文挑战第14天
20 1
|
27天前
|
消息中间件 NoSQL Kafka
大数据-116 - Flink DataStream Sink 原理、概念、常见Sink类型 配置与使用 附带案例1:消费Kafka写到Redis
大数据-116 - Flink DataStream Sink 原理、概念、常见Sink类型 配置与使用 附带案例1:消费Kafka写到Redis
97 0
|
30天前
|
NoSQL Ubuntu Linux
redis的基本安装配置启动使用
redis的基本安装配置启动使用
31 0
|
1月前
|
缓存 NoSQL 数据处理
原生php实现redis缓存配置和使用方法
通过上述步骤,你可以在PHP项目中配置并使用Redis作为高性能的缓存解决方案。合理利用Redis的各种数据结构和特性,可以有效提升应用的响应速度和数据处理效率。记得在实际应用中根据具体需求选择合适的缓存策略,如设置合理的过期时间,以避免内存过度消耗。
42 0
|
1月前
|
存储 NoSQL Java
Spring Boot项目中使用Redis实现接口幂等性的方案
通过上述方法,可以有效地在Spring Boot项目中利用Redis实现接口幂等性,既保证了接口操作的安全性,又提高了系统的可靠性。
32 0
|
3月前
|
NoSQL Redis 容器
【Azure Cache for Redis】Redis的导出页面无法配置Storage SAS时通过az cli来完成
【Azure Cache for Redis】Redis的导出页面无法配置Storage SAS时通过az cli来完成
|
3月前
|
缓存 NoSQL 测试技术
【Azure Redis 缓存】Azure Redis 功能性讨论三: 调优参数配置
【Azure Redis 缓存】Azure Redis 功能性讨论三: 调优参数配置
|
3月前
|
存储 缓存 NoSQL
【Azure Redis 缓存】由Azure Redis是否可以自定义密码而引申出Azure PaaS的Redis服务是否可以和自建的Redis进行主从配置呢?
【Azure Redis 缓存】由Azure Redis是否可以自定义密码而引申出Azure PaaS的Redis服务是否可以和自建的Redis进行主从配置呢?