暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

动态切换数据源

读读书写写代码 2021-09-30
526

背景

前面使用的sharding-jdbc模式,使用哪些数据源是静态配置的,可能也可以进行动态刷新,目前还没有找到明确的方法。这样的使用策略是基于库本身的高可用机制来保障的。

分库方式:根据特定的分库键进行分库,路由到特定库之后高可用依赖于库本身的高可用(主备从、主从切换等);

分表方式:同样依赖于库的高可用机制;

这样就引出一个问题,在多数据源的情况下,如果库挂了,一时起不来,如何确保正常做业务呢?应用程序能否根据配置(配置中心)或者运行时数据动态决定使用哪个数据源呢?

怎么能快速隔离某个出问题的数据源!!

思路

从外围的监控、埋点等数据源得到数据库挂了的消息之后,在配置中心修改相关配置,应用程序感知配置,执行对应的逻辑,隔离出问题的数据源。

查资料得知,spring提供了一个AbstractRoutingDataSource
类,可用于进行动态数据源切换,需要根据自身业务设置本数据源。

过程

创建DataSourceConfig

用于注入需要的数据源,相关配置信息从配置中心或者本地配置文件获取。

 package com.orderservice.datasource;
 
 import com.zaxxer.hikari.HikariConfig;
 import com.zaxxer.hikari.HikariDataSource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.cloud.context.config.annotation.RefreshScope;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.Configuration;
 
 import javax.sql.DataSource;
 import java.util.HashMap;
 import java.util.Map;
 
 /**
  * 数据源配置
  *
  * @author
  * @version 1.0
  * @date 2021-09-24 20:59
  */
 
 @Slf4j
 @ComponentScan
 @Configuration
 public class DataSourceConfig {
 
     /**
      * ds1相关配置
      */
     @Value("${spring.ds1.driver-class-name}")
     private String ds1DriverClass;
 
     @Value("${spring.ds1.url}")
     private String ds1JdbcUrl;
 
     @Value("${spring.ds1.username}")
     private String ds1UserName;
 
     @Value("${spring.ds1.password}")
     private String ds1PassWord;
 
     /**
      * ds2相关配置
      */
     @Value("${spring.ds2.driver-class-name}")
     private String ds2DriverClass;
 
     @Value("${spring.ds2.url}")
     private String ds2JdbcUrl;
 
     @Value("${spring.ds2.username}")
     private String ds2UserName;
 
     @Value("${spring.ds2.password}")
     private String ds2PassWord;
 
     private DataSource getDataSource1(){
         log.info("create datasource-1 by code.{},{},{},{}", ds1DriverClass, ds1JdbcUrl, ds1UserName, ds1PassWord);
         HikariConfig config = new HikariConfig();
         config.setDriverClassName(ds1DriverClass);
         config.setJdbcUrl(ds1JdbcUrl);
         config.setUsername(ds1UserName);
         config.setPassword(ds1PassWord);
         return new HikariDataSource(config);
    }
 
     public DataSource getDataSource2(){
         log.info("create datasource-2 by code.{},{},{},{}", ds2DriverClass, ds2JdbcUrl, ds2UserName, ds2PassWord);
         HikariConfig config = new HikariConfig();
         config.setDriverClassName(ds2DriverClass);
         config.setJdbcUrl(ds2JdbcUrl);
         config.setUsername(ds2UserName);
         config.setPassword(ds2PassWord);
         return new HikariDataSource(config);
    }
 
     @Bean
     @RefreshScope
     public OrderRoutingDataSource getDataSource(){
         Map<Object,Object> orderDataSources = new HashMap<>(2);
         DataSource ds1 = getDataSource1();
         DataSource ds2 = getDataSource2();
         orderDataSources.put("ds1",ds1);
         orderDataSources.put("ds2",ds2);
         OrderRoutingDataSource orderDataSource = new OrderRoutingDataSource();
         orderDataSource.setTargetDataSources(orderDataSources);
         orderDataSource.setDefaultTargetDataSource(ds1);
         return orderDataSource;
    }
 }
 

创建线程安全的获取数据源静态类DataSourceContext

外部调用设置,更新内部数据源,本例子总采取了ID低两位判断的方式,分为高半区数据库和低半区数据库;

 package com.orderservice.datasource;
 
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
 
 /**
  * 数据源切换上下文,保存所有数据源
  *
  * @author
  * @version 1.0
  * @date 2021-09-26 09:30
  */
 
 @Slf4j
 @Component
 public class DataSourceContext {
 
     private static final Long LOWER_ID = 50L;
     private static final ThreadLocal<String> DS_HOLDER = new ThreadLocal<>();
 
     public static void setDataSource(Long id){
         DS_HOLDER.remove();
         log.info("sharding key:{}",id);
 
         long result = id % 100;
         if(result < LOWER_ID){
             log.info("use datasource:ds1");
             DS_HOLDER.set("ds1");
        }else{
             log.info("use datasource:ds2");
             DS_HOLDER.set("ds2");
        }
    }
 
     public static String getDataSource(){
         return DS_HOLDER.get();
    }
 
     public static void clearDataSource(){
         DS_HOLDER.remove();
    }
 }
 

继承AbstractRoutingDataSource

生成自定义数据源,并定义数据源选择算法,从DataSourceContext
获取数据源。

 package com.orderservice.datasource;
 
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
 
 import java.util.HashMap;
 import java.util.Map;
 
 /**
  * 动态数据源实现
  *
  * @author
  * @version 1.0
  * @date 2021-09-27 19:18
  */
 
 @Slf4j
 public class OrderRoutingDataSource extends AbstractRoutingDataSource {
 
     private Map<Object,Object> orderDataSourrces = new HashMap<>();
 
     @Override
     public void setTargetDataSources(Map<Object, Object> targetDataSources) {
         super.setTargetDataSources(targetDataSources);
         orderDataSourrces.putAll(targetDataSources);
         super.afterPropertiesSet();
    }
 
     @Override
     protected Object determineCurrentLookupKey() {
         String dataSourceIndex = DataSourceContext.getDataSource();
         log.info("CurrentLookupKey:{}",dataSourceIndex);
         return dataSourceIndex;
    }
 
 }
 

使用方法

在具体使用中,在需要调用数据库的dbservice中,调用DataSourceContext.setDataSource
方法,传入当前ID,就能根据规则选择合适的数据源。还可以增加一个整体开关,用于是否分库的判断,开关数据保存在配置中心,如果开关打开,按照正常逻辑选择数据源,否则选择默认数据源。这样,可以在某个数据源出现问题的情况下,用默认数据源继续提供服务。

 
 @Value("${spring.datasource.autosharding:false}")
 private Boolean autoSharding;
 
 Long id = idService.nextID();
  log.info("id={},autosharding={}",id,autoSharding);
  if(!autoSharding){
     log.info("自动分库开关关闭,使用默认库");
     DataSourceContext.clearDataSource();
  }else{
     log.info("设置要使用的数据源");
     DataSourceContext.setDataSource(id);
  }

需要在需要自动刷新,需要在对应的类上增加 @RefreshScope注解。

继续改进

  • 以上过程,只能在ds2出问题时生效,ds1出现问题怎么处理还待改进,需要更灵活的方式确保任意数据源出问题时,能够灵活切换。

  • 分库的数据还是静态配置的,如果要增减数据源怎么处理,需要继续探讨。

  • 数据库探活机制,探活脚本自动检测数据库状态,并触发配置中心更改配置,才能达到较好的自动化效果。

  • 是否会误判,因为网络等原因,数据库全部连不上了,又该做何等处理?


文章转载自读读书写写代码,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论