实战分享: Spring boot 调用之间实现动态数据源

需求: 根据项目部署在不同的域名,在同一套系统下,分别访问不同的数据库

         (当然在看别人帖子的时候,也发现了不同接口访问不同数据源问题,就是分库动态数据源需求了,其实实现都一样)

业务描述:

  1. 部署的时候用的一套系统,分别部署不同的域名下
  2. 告诉用户1去  aaaaa.com访问这个系统,
  3. 告诉用户2去  bbbbb.com访问这个系统
  4. aaaaa.com产生的数据在DataSource1
  5. bbbbb.com产生的数据在DataSource2

整体自我评价: 啰里啰嗦,

画个简单的web微服务 / Data1微服务结构图吧,当然Data有很多不再此讨论范围内...

实现要点:

  1. 期间[web微服务]服务间的调用Date1微服务时,分析域名以后数据源标志放在服务之间的http请求header中,
  2. 在拦截器里面根据不同的业务现切换到不同的datasource(我这个就是);也有的会在业务层根据业务来自动切换。这种方案在多线程并发的时候会出现一些问题,需要使用threadlocal等技术来实现多线程竞争切换数据源的问题。
  3. 在被Data1提供数据端,写一个继承AbstractRoutingDataSource的类,这个类中必须实现抽象方法determineCurrentLookupKey,设置数据源标识
  4. 剩下就是AbstractRoutingDataSource在配置DataSource时的determineTargetDataSource的方法了
  5. 看源码吧

先看源码(摘的一部分)吧,这个determineTargetDataSource方法的上面的翻译(学渣级翻译)过来就是

检索当前目标数据源,先在determineCurrentLookupKey()抽象方法中查找关键字,在setTargetDataSources()中执行查找这个map找,再不行就返回指定的setDefaultTargetDataSource() 里面找一个DataSource返回回来

总之人家意思很明显,要不你给我关键字我自己找,找不到我就去默认的里面找,默认也没有的话我就断言null了呗

    
    @Nullable
	private Map<Object, Object> targetDataSources;

    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
		this.targetDataSources = targetDataSources;
	}  

    @Nullable
	private DataSource resolvedDefaultDataSource;
      /**
	 * Retrieve the current target DataSource. Determines the
	 * {@link #determineCurrentLookupKey() current lookup key}, performs
	 * a lookup in the {@link #setTargetDataSources targetDataSources} map,
	 * falls back to the specified
	 * {@link #setDefaultTargetDataSource default target DataSource} if necessary.
	 * @see #determineCurrentLookupKey()
	 */
	protected DataSource determineTargetDataSource() {
		Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
		Object lookupKey = determineCurrentLookupKey();
		DataSource dataSource = this.resolvedDataSources.get(lookupKey);
		if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
			dataSource = this.resolvedDefaultDataSource;
		}
		if (dataSource == null) {
			throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
		}
		return dataSource;
	}

    @Nullable
	protected abstract Object determineCurrentLookupKey();

 所以,就先继承了这个AbstractRoutingDataSource,顺着他的把一些内容覆盖掉就是了,

第一步是实现determineCurrentLookupKey()抽象方法,第二步找到this.resolvedDataSources变量的赋值,第三步this.resolvedDefaultDataSource变量的赋值

(this.lenientFallback默认是true)

分开搞定,第一步实现抽象方法好说先放放,第二步resolvedDataSources赋值顺着源码找找看,在120行有调用,

画一下重点:targetDataSources这个变量,一会重新赋值了它,再根据本方法的业务就完成了切换不同的数据源

本方法解析:简单分析一下(不往里细挖了),就是通过resolveSpecifiedDataSource()方法(通过关键字解析为数据源实例),解析出来的数据源放了resolvedDataSources(Map<Object,DataSource>类型) 进去,然后又检查了一下有没有配置默认的(Object   defaultTargetDataSource)数据源,有点话再通过resolveSpecifiedDataSource()(通过关键字解析为数据源实例)方法返回一个DataSource,放到resolvedDefaultDataSource中

       @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<Object, Object> 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<Object,Object>和key,就可以实现了不同数据源

(另说一下,其实这个Map<Object,Object>里面放的是Map<String,DataSource>) 

  • 开始拓展第二步:在写专门存放key的DataSourceContextHolder类,里面使用了ThreadLocal技术
//只对当前执行线程有效
public class DataSourceContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
    public static synchronized void setKey(String key){
        contextHolder.set(key);
    }
    public static String getCity(){
        return contextHolder.get();
    }
    public static void clearKey(){
        contextHolder.remove();
    }
}
  • 开始拓展第三步:通过拦截器,每次将通过上下文头取出关键字 放入DataSourceContextHolder类的key中,
//被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;
    }
}

(这个本来是抽象类,很多具体业务都实现然后再实现抽象方法去读配置/ 设定默认数据源/  查找配置内所有的数据源) ,我就不一一列举了,直接将实现类的三个方法代码搬进来了)

  • 再往下的类功能就是Bean注入上面DynamicDataSource类中setTargetDataSources方法,再使用的时候加载进去,并放入默认数据源
  • 里面最关键的是下面的两个bean方法,通过一层层注入,实现了所有代码的生效.

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<Object, Object> getDataSources();
    protected  Map<Object, Object> getDataSources(){
        Map<Object,Object> 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<Object, Object> 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层里面动态切换数据源。

 

参考来源:

  1. 基于注解的Spring多数据源配置和使用 
  2. Spring Boot 集成Mybatis实现多数据源
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值