查看原文
其他

【171期】面试官问:如何使用 Nacos 实现数据库连接的自动切换?

Java精选 2022-08-09

点击上方“Java精选”,选择“设为星标”

别问别人为什么,多问自己凭什么!

下方有惊喜留言必回,有问必答!

每天 08:35 更新文章,每天进步一点点...

为什么使用nacos?

Nacos作为参数配置中心,可以使服务在不重启的情况下动态修改配置参数。官网的描述更加详细点nacos动态配置服务,部分如下:

  • 动态配置服务可以让您以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置。

  • 动态配置消除了配置变更时重新部署应用和服务的需要,让配置管理变得更加高效和敏捷。

  • 配置中心化管理让实现无状态服务变得更简单,让服务按需弹性扩展变得更容易。

  • Nacos 提供了一个简洁易用的UI (控制台样例 Demo) 帮助您管理所有的服务和应用的配置。Nacos 还提供包括配置版本跟踪、金丝雀发布、一键回滚配置以及客户端配置更新状态跟踪在内的一系列开箱即用的配置管理特性,帮助您更安全地在生产环境中管理配置变更和降低配置变更带来的风险。

客户端如何监听nacos服务器配置的更新?

客户端使用http方式post请求,采用长轮询的方式,判断nacous服务器配置是否有更新。在nacos服务器端,采用队列存储客户端的请求,任务调度定时执行客户端的请求;当Nacos修改配置后,nacos服务器端会从队列中读取客户端的请求配置,并且立即把修改的配置写入返回给请求的客户端,配置的更改在客户端就立即生效,可以参考:

https://www.jianshu.com/p/acb9b1093a54

在 Spring Cloud 中,在 Environment 的属性配置发生变化时,会发布 EnvironmentChangeEvent 事件。这样,我们只需要实现 EnvironmentChangeEvent 事件的监听器,就可以进行自定义的逻辑处理。

数据库宕机后,如何更新nacos配置实现数据库连接的自动切换?

基于mybatis plus,一主一从的配置来实现数据源的配置的自动切换。mybatis plus的主库master,从库slave_1的配置如下:

spring.datasource.dynamic.primary=master
spring.datasource.dynamic.datasource.master.username=root
spring.datasource.dynamic.datasource.master.password=123456
spring.datasource.dynamic.datasource.master.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.dynamic.datasource.master.url=jdbc:mysql://127.0.0.1:3306/smcx_farm_products_mall?autoReconnectForPools=true&useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
 
spring.datasource.dynamic.datasource.slave_1.username=root
spring.datasource.dynamic.datasource.slave_1.password=root
spring.datasource.dynamic.datasource.slave_1.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.dynamic.datasource.slave_1.url=jdbc:mysql://127.0.0.1:3306/smcx_farm_products_mall?autoReconnectForPools=true&useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai

分析mybatis plus源码知道,mybatis plus定义DynamicRoutingDataSource实现数据源的动态切换,该类继承AbstractRoutingDataSource实现的,在获取数据库链接Connection时,根据配置判断从主从数据源获取连接,AbstractRoutingDataSource部分源码如下:

public abstract class AbstractRoutingDataSource extends AbstractDataSource {
 
  /**
   * 子类实现决定最终数据源
   *
   * @return 数据源
   */

  protected abstract DataSource determineDataSource();
 
  @Override
  public Connection getConnection() throws SQLException {
 
    return determineDataSource().getConnection();
  }
 
  @Override
  public Connection getConnection(String username, String password) throws SQLException {
    return determineDataSource().getConnection(username, password);
  }
}

分析mybatis plus配置类DynamicDataSourceAutoConfiguration,可以知道,只需要实现可以监听nacos参数配置,可以动态切换数据源配置的DataSource即可,DynamicDataSourceAutoConfiguration的部分源码如下:

@Configuration
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
@Import(DruidDynamicDataSourceConfiguration.class)
@ConditionalOnProperty(prefix 
= DynamicDataSourceProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
public class DynamicDataSourceAutoConfiguration {
 
    
 /**
   * 数据源配置
   *
   */

  @Autowired
  private DynamicDataSourceProperties properties;
 
 
  @Bean
  @ConditionalOnMissingBean
  public DynamicDataSourceProvider dynamicDataSourceProvider() {
    return new YmlDynamicDataSourceProvider(properties);
  }
 
 
/**
   * 在spring中注入动态数据源,注入自定义的DataSource即可扩展
   *
   */

  @Bean
  @ConditionalOnMissingBean
  public DataSource dataSource(DynamicDataSourceProvider dynamicDataSourceProvider) {
    DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
    dataSource.setPrimary(properties.getPrimary());
    dataSource.setStrategy(properties.getStrategy());
    dataSource.setProvider(dynamicDataSourceProvider);
    dataSource.setP6spy(properties.getP6spy());
    dataSource.setStrict(properties.getStrict());
    return dataSource;
  }
 }

定义DynamicRoutingAndSwitchingDataSource,实现数据源动态路由,数据库配置自动切换。采用装饰模式扩展mybatis plus的DynamicRoutingDataSource的功能,实现EnvironmentChangeEvent事件的监听器ApplicationListener,在nacos配置修改的时候,监听配置的修改。另外推荐Java面试资料,关注公众号Java精选,回复Java面试,获取最新面试资料,支持在线随时随地刷题。

Nacos配置在触发事件时,只会传输更新值的key/value值,所以,需要根据更新属性的key值,判断是否修改的数据库配置。其源码如下:

@Slf4j
public class DynamicRoutingAndSwitchingDataSource extends AbstractRoutingDataSource implements ApplicationListener<EnvironmentChangeEvent>, EnvironmentAware,
        InitializingBean, DisposableBean 
{
 
    /**
     * mybatis plus 动态数据源Properties
     */

    private DynamicDataSourceProperties dynamicDataSourceProperties;
    /**
     * 多数据源加载接口
     */

    private YmlDynamicDataSourceProvider dynamicDataSourceProvider;
    /**
     * 动态数据源Properties 属性key集合
     */

    private Set<String> propertyKeys;
    /**
     * mybatis plus动态路由数据源,使用装饰模式修饰
     */

    private DynamicRoutingDataSource dynamicRoutingDataSource;
    /**
     * spring Environment 配置对象
     */

    private Environment environment;
 
    public DynamicRoutingAndSwitchingDataSource(DynamicDataSourceProperties properties, DynamicDataSourceProvider provider) {
 
        this.dynamicDataSourceProperties = properties;
        this.dynamicDataSourceProvider = (YmlDynamicDataSourceProvider) provider;
        this.dynamicRoutingDataSource = this.buildDynamicRoutingDataSource(properties, provider);
        this.propertyKeys = Sets.newHashSet(
                ConfigProperty.MASTER_USERNAME_KEY, ConfigProperty.MASTER_PASSWORD_KEY,
                ConfigProperty.MASTER_DRIVER_CLASS_NAME_KEY, ConfigProperty.MASTER_RL_KEY,
                ConfigProperty.SLAVE_1_USERNAME_KEY, ConfigProperty.SLAVE_1_PASSWORD_KEY,
                ConfigProperty.SLAVE_1_DRIVER_CLASS_NAME_KEY, ConfigProperty.SLAVE_1_RL_KEY);
 
    }
 
    private DynamicRoutingDataSource buildDynamicRoutingDataSource(DynamicDataSourceProperties dynamicDataSourceProperties, DynamicDataSourceProvider provider) {
 
        DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
        dataSource.setPrimary(dynamicDataSourceProperties.getPrimary());
        dataSource.setStrategy(dynamicDataSourceProperties.getStrategy());
        dataSource.setProvider(provider);
        dataSource.setP6spy(dynamicDataSourceProperties.getP6spy());
        dataSource.setStrict(dynamicDataSourceProperties.getStrict());
        return dataSource;
    }
   /**
     * 监听配置修改的EnvironmentChangeEvent事件
     */

    @Override
    public void onApplicationEvent(EnvironmentChangeEvent event) {
 
        Set<String> keys;
        if (isNotDatasourceConfigChanged(keys = event.getKeys())) {
 
            log.info("keys {} are not database config!", JSON.toJSONString(keys));
            return;
        }
        DynamicDataSourceProperties refreshProperties = this.dynamicDataSourceProperties;
        Map<String, DataSourceProperty> datasource = refreshProperties.getDatasource();
        Map<String, Properties> changedPropertiesMap = this.getChangedProperties();
 
        if (log.isDebugEnabled()) {
            log.debug("changed properties {}", JSON.toJSONString(changedPropertiesMap));
        }
        for (Map.Entry<String, DataSourceProperty> entry : datasource.entrySet()) {
 
            String nodeName = entry.getKey();
            Properties properties;
            if (Objects.isNull(properties = changedPropertiesMap.get(nodeName))) {
 
                log.info("node {} has no properties!changedPropertiesMap {}", nodeName, JSON.toJSONString(changedPropertiesMap));
                continue;
            }
            DataSourceProperty dataSourceProperty = entry.getValue();
            String userName;
            if (StringUtils.isNotBlank(userName = properties.getProperty(ConfigProperty.USERNAME))) {
                dataSourceProperty.setUsername(userName);
            }
            String password;
            if (StringUtils.isNotBlank(password = properties.getProperty(ConfigProperty.PASSWORD))) {
                dataSourceProperty.setPassword(password);
            }
            String driverClassName;
            if (StringUtils.isNotBlank(driverClassName = properties.getProperty(ConfigProperty.DRIVER_CLASS_NAME))) {
                dataSourceProperty.setDriverClassName(driverClassName);
            }
            String url;
            if (StringUtils.isNotBlank(url = properties.getProperty(ConfigProperty.URL))) {
                dataSourceProperty.setUrl(url);
            }
        }
 
        try {
 
            // 使用反射设置properties,避免初始化后,spring自动注入的DynamicDataSourceCreator无法获取
            // @Autowired
            // private DynamicDataSourceCreator dynamicDataSourceCreator;
            Reflections.setFieldValue(this.dynamicDataSourceProvider, "properties", refreshProperties);
            this.dynamicRoutingDataSource = this.buildDynamicRoutingDataSource(refreshProperties, this.dynamicDataSourceProvider);
            // 重置数据源,公众号Java精选,有惊喜!
            this.dynamicRoutingDataSource.afterPropertiesSet();
        } catch (Exception e) {
 
            throw new RuntimeException("refresh dynamic routing switch datasource error!", e);
        }
    }
 
    private boolean isNotDatasourceConfigChanged(Set<String> keys) {
 
        for (String key : keys) {
            if (propertyKeys.contains(key)) {
                return false;
            }
        }
        return true;
    }
 
    private boolean isMasterKey(String key) {
        return key.contains(ConfigProperty.MASTER);
    }
 
    private boolean isSlave1Key(String key) {
        return key.contains(ConfigProperty.SLAVE_1);
    }
 
    private boolean isUsernameKey(String key) {
        return key.contains(ConfigProperty.USERNAME);
    }
 
    private boolean isPasswordKey(String key) {
        return key.contains(ConfigProperty.PASSWORD);
    }
 
    private boolean isDriverClassNameKey(String key) {
        return key.contains(ConfigProperty.DRIVER_CLASS_NAME);
    }
 
    private boolean isUrlKey(String key) {
        return key.contains(ConfigProperty.URL);
    }
 
    private Map<String, Properties> getChangedProperties() {
 
        Map<String, Properties> propertiesMap = ImmutableMap.of(ConfigProperty.MASTER, new Properties(), ConfigProperty.SLAVE_1, new Properties());
        for (String key : this.propertyKeys) {
 
            String value;
            if (StringUtils.isBlank(value = this.environment.getProperty(key))) {
                continue;
            }
            String propertyKey = this.convert2propertyKey(key);
            if (this.isMasterKey(key)) {
                propertiesMap.get(ConfigProperty.MASTER).setProperty(propertyKey, value);
            } else if (isSlave1Key(key)) {
                propertiesMap.get(ConfigProperty.SLAVE_1).setProperty(propertyKey, value);
            }
        }
        return propertiesMap;
    }
 
    private String convert2propertyKey(String key) {
 
        String propertyKey = key;
        if (this.isUsernameKey(key)) {
            propertyKey = ConfigProperty.USERNAME;
        } else if (this.isPasswordKey(key)) {
            propertyKey = ConfigProperty.PASSWORD;
        } else if (this.isDriverClassNameKey(key)) {
            propertyKey = ConfigProperty.DRIVER_CLASS_NAME;
        } else if (this.isUrlKey(key)) {
            propertyKey = ConfigProperty.URL;
        }
        return propertyKey;
    }
 
   /**
     * 设置Environment 属性
     */

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }
 
    /**
     * 调用被装饰的dynamicRoutingDataSource的方法,获取指定的数据源
     */

    @Override
    protected DataSource determineDataSource() {
        return this.dynamicRoutingDataSource.determineDataSource();
    }
 
   /**
     * 调用被装饰的dynamicRoutingDataSource的方法,初始化数据源
     */

    @Override
    public void afterPropertiesSet() throws Exception {
        this.dynamicRoutingDataSource.afterPropertiesSet();
    }
 
    @Override
    public void destroy() throws Exception {
        this.dynamicRoutingDataSource.destroy();
    }
}
 
public interface ConfigProperty {
 
     //数据源属性配置 
    String MASTER = "master";
    String SLAVE_1 = "slave_1";
    String USERNAME = "username";
    String PASSWORD = "password";
    String DRIVER_CLASS_NAME = "driver-class-name";
    String URL = "url";
 
     //数据源属性key配置 
    String MASTER_USERNAME_KEY = "spring.datasource.dynamic.datasource.master." + USERNAME;
    String MASTER_PASSWORD_KEY = "spring.datasource.dynamic.datasource.master." + PASSWORD;
    String MASTER_DRIVER_CLASS_NAME_KEY = "spring.datasource.dynamic.datasource.master." + DRIVER_CLASS_NAME;
    String MASTER_RL_KEY = "spring.datasource.dynamic.datasource.master." + URL;
    String SLAVE_1_USERNAME_KEY = "spring.datasource.dynamic.datasource.slave_1." + USERNAME;
    String SLAVE_1_PASSWORD_KEY = "spring.datasource.dynamic.datasource.slave_1." + PASSWORD;
    String SLAVE_1_DRIVER_CLASS_NAME_KEY = "spring.datasource.dynamic.datasource.slave_1." + DRIVER_CLASS_NAME;
    String SLAVE_1_RL_KEY = "spring.datasource.dynamic.datasource.slave_1." + URL;
}

自定义DynamicDataSourceAutoConfiguration,在spring中注入定义的DynamicRoutingAndSwitchingDataSource,其源码如下:

@Configuration
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
@Import(DruidDynamicDataSourceConfiguration.class)
@ConditionalOnProperty(prefix 
= DynamicDataSourceProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
public class DynamicDataSourceAutoConfiguration {
 
    @Autowired
    private DynamicDataSourceProperties properties;
 
    @Bean
    public DataSource dataSource(DynamicDataSourceProvider dynamicDataSourceProvider) {
 
        DynamicRoutingAndSwitchingDataSource dataSource = new DynamicRoutingAndSwitchingDataSource(properties, dynamicDataSourceProvider);
        return dataSource;
    }
}

结果测试,在nacos服务器修改从数据库的配置,如图:

代码调试时,后端数据库配置生效信息,如图:

版权声明:本文为CSDN博主「iloveoverfly」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

https://blog.csdn.net/new_com/article/details/108152630

公众号“Java精选”所发表内容注明来源的,版权归原出处所有(无法查证版权的或者未注明出处的均来自网络,系转载,转载的目的在于传递更多信息,版权属于原作者。如有侵权,请联系,笔者会第一时间删除处理!
------ THE END ------

精品资料,超赞福利!


3000+ 道面试题在线刷,最新、最全 Java 面试题!

期往精选  点击标题可跳转

【164期】面试官问:为什么生产环境中,建议禁用 Redis 的 keys 命令?

【165期】关于 5 道 String 面试题,全答对的面试者不足 10%

【166期】面试官问:为什么不建议使用 @Async 注解?

【167期】Java8 新特性:使用 Stream 流递归实现遍历树形结构

【168期】面试官问:如果 MySQL 的自增 ID 用完了,怎么办?

【169期】面试官问:说说为什么要限流,有哪些解决方案?

【170期】MySQL 定时备份数据库(非常全),值得收藏!

【171期】面试官问:Spring 注解 @After,@Around,@Before 的执行顺序是?

技术交流群!

最近有很多人问,有没有读者交流群,想知道如何加入。加入方式很简单,有兴趣的同学,只需要点击下方卡片,回复“加群”,即可免费加入我们的高质量技术交流群!

文章有帮助的话,在看,转发吧!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存