多数据场景: 在业务操作中需要对多个数据库进行访问或者操作。
原理: 实现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在执行事务切面的时候,就会将数据源进行绑定,所以,不要将数据源切换操作放在事务内,是没有用的。