背景
前面使用的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出现问题怎么处理还待改进,需要更灵活的方式确保任意数据源出问题时,能够灵活切换。
分库的数据还是静态配置的,如果要增减数据源怎么处理,需要继续探讨。
数据库探活机制,探活脚本自动检测数据库状态,并触发配置中心更改配置,才能达到较好的自动化效果。
是否会误判,因为网络等原因,数据库全部连不上了,又该做何等处理?




