spring配置多数据源

在这里插入图片描述
多数据场景: 在业务操作中需要对多个数据库进行访问或者操作。
原理: 实现AbstractRoutingDataSource接口,
其中包含两个比较重要的属性

	private Map<Object, Object> targetDataSources;  最终的数据源

	private Object defaultTargetDataSource; 默认数据源
	
	private boolean lenientFallback = true;

	private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();

	private Map<Object, DataSource> resolvedDataSources;

	private DataSource resolvedDefaultDataSource;

这个类实现了InitializingBean 接口,并且重写了afterPropertiesSet方法
这个方法将会在spring的bean都创建好,并且都注入以后才会进行调用。
所以我们可以看到在spring初始化以后,会将targetDataSources这个map进行遍历,并且将对应额key,value放入另一个map中。

	@Override
	public void afterPropertiesSet() {
		if (this.targetDataSources == null) {
			throw new IllegalArgumentException("Property 'targetDataSources' is required");
		}
		this.resolvedDataSources = new HashMap<Object, DataSource>(this.targetDataSources.size());
		for (Map.Entry<Object, Object> entry : this.targetDataSources.entrySet()) {
		    //返回这个key
			Object lookupKey = resolveSpecifiedLookupKey(entry.getKey());
			//验证是否为数据源 
			DataSource dataSource = resolveSpecifiedDataSource(entry.getValue());
			//放入另一个map中
			this.resolvedDataSources.put(lookupKey, dataSource);
		}
		if (this.defaultTargetDataSource != null) {
		// 不为空  通过默认数据源的key 设置默认的数据源 
			this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
		}
	}

在这个类中可以看到 用了一个map来存储数据源信息,key对应着数据源的key,value则是一个具体的数据源对象。
里面真正决定使用哪个数据源的方法:

protected DataSource determineTargetDataSource() {
		Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
		//通过determineCurrentLookupKey方法返回一个object
		Object lookupKey = determineCurrentLookupKey();
		//通过返回的key去map找到对应的数据源
		DataSource dataSource = this.resolvedDataSources.get(lookupKey);
		// 为空 并且key也为空  给一个默认的数据源
		if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
			dataSource = this.resolvedDefaultDataSource;
		}
		//key不为空  datascourse为空 异常
		if (dataSource == null) {
			throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
		}
		return dataSource;
	}

可以看到最重要的是determineCurrentLookupKey方法,据此我们可以选择通过实现AbstractRoutingDataSource接口来重写这个方法,进而制定规则来决定使用哪个数据源。

大体的思路就是这样。

先来看配置

数据源1
<bean id="datasource01"
		class="org.apache.commons.dbcp.BasicDataSource">
		<property name="driverClassName"
			value="${datasource01.db.driverClassName}" />
		<property name="url" value="${datasource01.db.url}" />
		<property name="username" value="${atasource01.db.username}" />
		<property name="password" value="${atasource01.db.password}" />
		<property name="maxActive" value="5"></property>
		<property name="maxIdle" value="0"></property>
		<property name="maxWait" value="2000"></property>
		<property name="defaultAutoCommit" value="false"></property>
		<property name="validationQuery" value="select 1 from dual" />
	</bean>

. 数据源2
	<bean id="datasource02"
		class="org.apache.commons.dbcp.BasicDataSource">
		<property name="driverClassName"
			value="${datasource02.db.driverClassName}" />
		<property name="url" value="${atasource02.db.url}" />
		<property name="username" value="${datasource02.db.username}" />
		<property name="password" value="${datasource02.db.password}" />
		<property name="maxActive" value="5"></property>
		<property name="maxIdle" value="0"></property>
		<property name="maxWait" value="2000"></property>
		<property name="defaultAutoCommit" value="false"></property>
		<property name="validationQuery" value="select 1 from dual" />
	</bean>
	
	数据源3
	<bean id="datasource03" class="org.apache.commons.dbcp.BasicDataSource">
		<property name="driverClassName"
			value="${datasource03.db.driverClassName}" />
		<property name="url" value="${datasource03.db.url}" />
		<property name="username" value="${datasource03.db.username}" />
		<property name="password" value="${datasource03.db.password}" />
		<property name="maxActive" value="5"></property>
		<property name="maxIdle" value="0"></property>
		<property name="maxWait" value="2000"></property>
		<property name="defaultAutoCommit" value="false"></property>
		<property name="validationQuery" value="select 1 from dual" />
	</bean>
	
	
  // 配置动态数据源  这个类就是继承AbstractRoutingDataSource
	<bean id="dataSource" class="com.ly.config.dataSource.DynamicDataSource">
	       //AbstractRoutingDataSource 类中的targetDataSources 属性 是一个map
		<property name="targetDataSources">
		   // private Map<Object, Object> targetDataSources;
			<map key-type="java.lang.String">
				<entry key="datasource01" value-ref="datasource01" />
				<entry key="datasource02" value-ref="datasource02" />
				<entry key="datasource03" value-ref="datasource02" />
			</map>
		</property>
		//private Object defaultTargetDataSource;
		<property name="defaultTargetDataSource" ref="datasource01" />
	</bean>

	<bean id="sqlSessionFactory"
		class="org.mybatis.spring.SqlSessionFactoryBean">
		<!--指定要用到的连接池 -->
		//连接池中注入动态数据源
		<property name="dataSource" ref="dataSource" />
	</bean>

这样我们就将三个数据源配置到了DynamicDataSource类中,数据源中的url,username等属性由properties中获取。DynamicDataSource类继承了AbstractRoutingDataSource,同时重写了determineCurrentLookupKey方法来决定具体用哪个数据源。
我们当然可以在determineCurrentLookupKey制定一系列的规则来指定数据源的key。或者说在业务方法中通过这个方法显示的去调用这个方法来设置key。但是这样切换数据源的方法就会穿插在业务方法中,很不合理。所以选择通过spring的aop将切换数据源的操作剥离出来。

public class DynamicDataSource extends AbstractRoutingDataSource {
	
		@Override
	protected Object determineCurrentLookupKey() {
		// TODO Auto-generated method stub
		 String dataSouceKey = DynamicDataSourceHolder.getDataSouce();  
	     return dataSouceKey; 
	}

}

这里 ,一般选择ThreadLocal来保存数据源的key,具体原理还在研究~~

public class DynamicDataSourceHolder {
	  // 保存数据源的key
	 public static final ThreadLocal<String> holder = new ThreadLocal<String>();  
	  
	    public static void putDataSource(String name) {  
	        holder.set(name);  
	    }  
	  
	    public static String getDataSouce() {  
	        return holder.get();  
	    }  
	    public static void clearDataSource(){
			holder.remove();
	 	}
}

通过注解的方式标记哪个方法需要切换数据源,首先我们先要有一个自定义注解

@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.METHOD) 
public @interface DataSource {
	String value(); 
}

定义一个类,来表明当方法上有这个注解的时候,我们要做什么

public class DataSourceAspect {

	public static Logger logger = Logger.getLogger(DataSourceAspect.class);

	public void before(JoinPoint point) {
		
		Object target = point.getTarget();
		String method = point.getSignature().getName();

		Class<?>[] classz = target.getClass().getInterfaces();

		Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
		try {
			//通过反射来获取方法上的注解
			Method m = classz[0].getMethod(method, parameterTypes);		
			// 不为空 且方法上有DataSource 注解 就切数据源 否则就用默认数据源
			if (m != null && m.isAnnotationPresent(DataSource.class)) {
				DataSource data = m.getAnnotation(DataSource.class);
				DynamicDataSourceHolder.putDataSource(data.value());
				logger.info("Use the specified data source [ " + data.value() + " ]");
			} else {
				DynamicDataSourceHolder.putDataSource(ConfigConstants.BASKGROUND_DATABASE);
				logger.info("Use the default data source [ " + ConfigConstants.BASKGROUND_DATABASE + " ]");
			}

		} catch (Exception e) {
			e.printStackTrace();
		}
	}

		
	public void after(){
		logger.info("------------reday to return method and clear datasource------------------");
		DynamicDataSourceHolder.clearDataSource();
		logger.info("------------clear datasource complete 100%------------------");

	}
}

最后在配置文件中配置这个切面的切点

	<!--动态数据源切入service层  -->
	//利用order  将数据源切换切面的执行在事务方法之前
	//首先将切面引入
	<bean id="manyDataSourceAspect" class="com.lyconfig.dataSource.DataSourceAspect"/>  
	//配置aop
    <aop:config>  
        <aop:aspect id="c" ref="manyDataSourceAspect" order="0">  
            <aop:pointcut id="txdata" expression="execution(* com.uplus.gateway..*Service*Impl.*(..))"/>
            <aop:before pointcut-ref="txdata" method="before"/>  
        </aop:aspect> 
    </aop:config>  

我这里配置的是在service层的接口上标记注解,就可以实现数据源的切换。

	@DataSource("datasource01")
    int insertSelective(CaseInfo record);

	
	@DataSource("datasource01")
    CaseInfo selectByPrimaryKey(Long serialNo);

大致流程就是 如果我们有一个dao层配置到了aop的切点表达式中, 在执行这个dao层方法之前,就是先执行DataSourceAspect 类中的before方法,然后将 @DataSource(“datasource01”) 注解中的datasource01取出,然后放入DynamicDataSourceHolder 类中的ThreadLocal 中,然后在获取数据源连接的时候,就会通过我们给的datasource01 作为key从map中对应的数据源 即数据源datasource01 。
然后使用这个数据源。这样就实现了数据源的切换。

要注意的问题!!!!
1.我这里选择的切入到service层,一般来说我们的业务方法都是写在service中的,事务也切到service层。所以如果不是分布式的事务,一定要注意切换数据源后,一旦出现异常,之前执行的方法要有对应的回退机制,保证数据的完整性。
2. 在一个事务中不要做数据源的切换,很可能使得数据源切换失效,导致xx表不存在异常。目前发现的情况是这样,具体的还在查。
3. 多数据源肯定是为了解决多个库的问题,我碰到过多个库的几张表要在一起进行联查,这就比较尴尬了。最傻的一次是在其中一个库建立了另一个库的同样的一张表,将数据冗余一份,然后查询的时候查询冗余库,,,结果被批了半天。 在网上查,有说本地建立同样的表结构,然后使用mysql的FEDERATED引擎来远程连接,不过没有具体操作,主要还是对FEDERATED这个引擎的不了解,不知道产线上会不会出问题。我这里因为另一个表的数据量比较少,而且不经常进行修改,新增等操作。然后将其放在了redis中,然后再进行联合查询或者是其他的操作。这个要看具体的业务了。
4.多数据源尽量个分布式事务进行搭配只用。2PC,3PC等。
5.spring在执行事务切面的时候,就会将数据源进行绑定,所以,不要将数据源切换操作放在事务内,是没有用的。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值