如何使用Nacos实现数据库自动容灾?

泄露秘密 提交于 2020-10-06 02:05:37

为什么使用nacos?

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

动态配置服务可以让您以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置。
动态配置消除了配置变更时重新部署应用和服务的需要,让配置管理变得更加高效和敏捷。
配置中心化管理让实现无状态服务变得更简单,让服务按需弹性扩展变得更容易。
Nacos 提供了一个简洁易用的UI (控制台样例 Demo) 帮助您管理所有的服务和应用的配置。Nacos 还提供包括配置版本跟踪、金丝雀发布、一键回滚配置以及客户端配置更新状态跟踪在内的一系列开箱即用的配置管理特性,帮助您更安全地在生产环境中管理配置变更和降低配置变更带来的风险。

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

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

在 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配置修改的时候,监听配置的修改。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);
            // 重置数据源
            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服务器修改从数据库的配置,如图:

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

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!