需求: 根据项目部署在不同的域名,在同一套系统下,分别访问不同的数据库
(当然在看别人帖子的时候,也发现了不同接口访问不同数据源问题,就是分库动态数据源需求了,其实实现都一样)
业务描述:
整体自我评价: 啰里啰嗦,
画个简单的web微服务 / Data1微服务结构图吧,当然Data有很多不再此讨论范围内...
实现要点:
先看源码(摘的一部分)吧,这个determineTargetDataSource方法的上面的翻译(学渣级翻译)过来就是
检索当前目标数据源,先在determineCurrentLookupKey()抽象方法中查找关键字,在setTargetDataSources()中执行查找这个map找,再不行就返回指定的setDefaultTargetDataSource() 里面找一个DataSource返回回来
总之人家意思很明显,要不你给我关键字我自己找,找不到我就去默认的里面找,默认也没有的话我就断言null了呗
@Nullable
private Map
所以,就先继承了这个AbstractRoutingDataSource,顺着他的把一些内容覆盖掉就是了,
第一步是实现determineCurrentLookupKey()抽象方法,第二步找到this.resolvedDataSources变量的赋值,第三步this.resolvedDefaultDataSource变量的赋值
(this.lenientFallback默认是true)
分开搞定,第一步实现抽象方法好说先放放,第二步resolvedDataSources赋值顺着源码找找看,在120行有调用,
本方法解析:简单分析一下(不往里细挖了),就是通过resolveSpecifiedDataSource()方法(通过关键字解析为数据源实例),解析出来的数据源放了resolvedDataSources(Map
@Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = resolveSpecifiedLookupKey(key);
DataSource dataSource = resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
第一步是实现determineCurrentLookupKey()抽象方法,第二步找到this.resolvedDataSources变量的赋值,第三步this.resolvedDefaultDataSource变量的赋值
(第二步完成过程:当调用方法给了this.targetDataSources时,再调用super.afterPropertiesSet()就给了this.resolvedDataSources的值了
第三步给默认数据源,没有实现,留着其他类在调用的时候,再set进去)
public class DynamicDataSource extends AbstractRoutingDataSource {
private static DynamicDataSource instance;
private static byte[] lock=new byte[0];
@Override
public void setTargetDataSources(Map targetDataSources) {
super.setTargetDataSources(targetDataSources);
// 必须添加该句,让方法根据重新赋值的targetDataSource依次根据key关键字
// 查找数据源,返回DataSource,否则新添加数据源无法识别到
super.afterPropertiesSet();
}
public static synchronized DynamicDataSource getInstance(){
if(instance==null){
synchronized (lock){
if(instance==null){
instance=new DynamicDataSource();
}
}
}
return instance;
}
// 实现其抽象方法,
// 因为在创建DataSource这个方法:determineTargetDataSource()中(上面有分析)
// 会调用这个key关键字,根据这个key在重新赋值的targetDataSource里面找DataSource
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getKey();
}
}
(另说一下,其实这个Map
//只对当前执行线程有效
public class DataSourceContextHolder {
private static final ThreadLocal contextHolder = new ThreadLocal();
public static synchronized void setKey(String key){
contextHolder.set(key);
}
public static String getCity(){
return contextHolder.get();
}
public static void clearKey(){
contextHolder.remove();
}
}
//被HTTP调用方使用的拦截器
//数据库关键字信息通过HTTP header头传递。
public class DataInterceptor implements HandlerInterceptor {
private static Logger logger = LoggerFactory.getLogger(DataInterceptor.class);
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object o) {
String key = request.getHeader("TEST");
logger.info("切换数据源:" + key);
DataSourceContextHolder.setKey(key);
return true;
}
}
(这个本来是抽象类,很多具体业务都实现然后再实现抽象方法去读配置/ 设定默认数据源/ 查找配置内所有的数据源) ,我就不一一列举了,直接将实现类的三个方法代码搬进来了)
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import com.alibaba.druid.pool.DruidDataSource;
/*
* 数据源配置抽象类
*/
public class DataSourceConfig {
@Autowired
private Environment env;
@Value("${datasources}")
private String datasources;
/*
* 默认数据源。
*/
//protected abstract DataSource getDefaultDataSource()
protected DataSource getDefaultDataSource(){
DruidDataSource defaultDS = new DruidDataSource();
defaultDS.setUrl(env.getProperty("spring.datasource.druid.url"));
defaultDS.setUsername(env.getProperty("spring.datasource.druid.username"));
defaultDS.setPassword(env.getProperty("spring.datasource.druid.password"));
defaultDS.setDriverClassName(env.getProperty("spring.datasource.druid.driver-class-name"));
return defaultDS;
}
/*
* 可动态路由的数据源。
*/
//protected abstract Map getDataSources();
protected Map getDataSources(){
Map map = new HashMap();
if (datasources != null && datasources.length() > 0) {
String[] names = datasources.split(",");
for (String name : names) {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(env.getProperty("spring.datasource." + name + ".url"));
dataSource.setUsername(env.getProperty("spring.datasource." + name + ".username"));
dataSource.setPassword(env.getProperty("spring.datasource." + name + ".password"));
dataSource.setDriverClassName(env.getProperty("spring.datasource." + name + ".driver-class-name"));
map.put(name, dataSource);
}
}
return map;
}
/*
* Mapper 文件位置。
*/
//protected abstract String getMapperLocation();
protected String getMapperLocation(){
return "classpath*:mapping/*.xml";
}
@Bean
public DynamicDataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = DynamicDataSource.getInstance();
Map dataSources = getDataSources();
if (dataSources.size() > 0) {
dynamicDataSource.setTargetDataSources( dataSources );
}
DataSource ds = getDefaultDataSource();
if (ds != null) {
dynamicDataSource.setDefaultTargetDataSource( ds );
}
return dynamicDataSource;
}
@Bean(name = "sqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate(
@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean
public SqlSessionFactory sqlSessionFactory(
@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dynamicDataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources( getMapperLocation() ));
return bean.getObject();
}
}
注意事项(来源于2)
spring的事务管理,是基于数据源的,所以如果要实现动态数据源切换,而且在同一个数据源中保证事务是起作用的话,就需要注意二者的顺序问题,即:在事物起作用之前就要把数据源切换回来。
举一个例子:web开发常见是三层结构:controller、service、dao。一般事务都会在service层添加,如果使用spring的声明式事物管理,在调用service层代码之前,spring会通过aop的方式动态添加事务控制代码,所以如果要想保证事物是有效的,那么就必须在spring添加事务之前把数据源动态切换过来,也就是动态切换数据源的aop要至少在service上添加,而且要在spring声明式事物aop之前添加.根据上面分析:
最简单的方式是把动态切换数据源的aop加到controller层,这样在controller层里面就可以确定下来数据源了。不过,这样有一个缺点就是,每一个controller绑定了一个数据源,不灵活。对于这种:一个请求,需要使用两个以上数据源中的数据完成的业务时,就无法实现了。
针对上面的这种问题,可以考虑把动态切换数据源的aop放到service层,但要注意一定要在事务aop之前来完成。这样,对于一个需要多个数据源数据的请求,我们只需要在controller里面注入多个service实现即可。但这种做法的问题在于,controller层里面会涉及到一些不必要的业务代码,例如:合并两个数据源中的list…
此外,针对上面的问题,还可以再考虑一种方案,就是把事务控制到dao层,然后在service层里面动态切换数据源。
参考来源: