Spring动态数据源配置
1. xml配置 [代码片段]
<!--动态数据源-->
<bean id="dataSource" class="com.greenline.health.common.dbconfiguration.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="ds001" value-ref="dataSource1"></entry>
<entry key="ds002" value-ref="dataSource2"></entry>
</map>
</property>
<property name="defaultTargetDataSource" ref="ds001"></property>
</bean>
<!-- DataSource定义。 -->
<bean name="dataSource1" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!-- DataSource定义。 -->
<bean name="dataSource2" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="url" value="${remote.jdbc.url}"/>
<property name="username" value="${remote.jdbc.username}"/>
<property name="password" value="${remote.jdbc.password}"/>
</bean>
<!--mybatis相关配置-->
<!-- 配置sqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 实例化sqlSessionFactory时需要使用上述配置好的数据源以及SQL映射文件 -->
<property name="dataSource" ref="dataSource"/>
<!-- 自动扫描/src/main/resources/sqlmap和sqlmap2/目录下的所有SQL映射的xml文件, 省掉Configuration.xml里的手工配置 value="classpath:me/gacl/mapping/*.xml"指的是classpath(类路径)下me.gacl.mapping包中的所有xml文件
UserMapper.xml位于me.gacl.mapping包下,这样UserMapper.xml就可以被自动扫描 -->
<property name="mapperLocations">
<array>
<value>classpath*:sqlmap/**/*.xml</value>
<value>classpath*:sqlmap2/**/*.xml</value>
</array>
</property>
<property name="plugins">
<list></list>
</property>
</bean>
<!-- 配置扫描器 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 扫描me.gacl.dao这个包以及它的子包下的所有映射接口类 -->
<property name="basePackage" value="com.greenline.health.dal*"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
<!--事务管理器-->
<!-- 配置Spring的事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--事务拦截配置-->
<!-- 拦截器方式配置事物 -->
<tx:advice id="transactionAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="add*" propagation="REQUIRED"/>
<tx:method name="append*" propagation="REQUIRED"/>
<tx:method name="insert*" propagation="REQUIRED"/>
<tx:method name="save*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
<tx:method name="modify*" propagation="REQUIRED"/>
<tx:method name="edit*" propagation="REQUIRED"/>
<tx:method name="get*" propagation="SUPPORTS"/>
<tx:method name="find*" propagation="SUPPORTS"/>
<tx:method name="load*" propagation="SUPPORTS"/>
<tx:method name="search*" propagation="SUPPORTS"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="transactionPointcut" expression="execution(* com.greenline.health.service..*Impl.*(..))"/>
<aop:advisor pointcut-ref="transactionPointcut" advice-ref="transactionAdvice"/>
</aop:config>
2. 动态数据源用到的代码类
//========数据源获取==============
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}
//数据源的拦截和切换
Component
Aspect
public class DBChangeInterceptor {
Logger logger = LoggerFactory.getLogger(DBChangeInterceptor.class);
@Pointcut(value = "execution(* com.greenline.health.dal..*.*(..))")
public void dbChangeBstKaPointCut() {
}
@Pointcut(value = "execution(* com.greenline.health.dal2..*.*(..))")
public void dbChangeStdLawPointCut() {
}
@Before(value = "dbChangeBstKaPointCut()")
public void changeBstKaDB(JoinPoint joinPoint) {
logger.debug("----------------调用ds001数据库连接----------------");
DataSourceContextHolder.setDataSourceType(DataSourceConstans.BTSKA);
}
@Before(value = "dbChangeStdLawPointCut()")
public void changeStdLawDB(JoinPoint joinPoint) {
logger.debug("----------------调用ds002数据库连接----------------");
DataSourceContextHolder.setDataSourceType(DataSourceConstans.STDLAW);
}
//数据源切换工具
public class DataSourceContextHolder {
private static final ThreadLocal contextHolder = new ThreadLocal(); // 线程本地环境
// 设置数据源类型
public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}
// 获取数据源类型
public static String getDataSourceType() {
return (String) contextHolder.get();
}
// 清除数据源类型
public static void clearDataSourceType() {
contextHolder.remove();
}
}
到此为止,配置已经完成,正常情况已经可以正确切换
为什么说正常情况?
很多时候,会遇到很多特别的情况,很简单的一个条件:加入事务
众所周知,一般事务配置在service层,但是动态数据源切换却是拦截在dao层
这时如果有事务可见问题就很难搞了.因为事务开启,调用进入service层首先去拿事务,
拿到事务之后,再去调用dao层对应的业务,这个时候才走到获取动态数据源. 获取动态数据源的时候,已经在事务中获取到了事务,就不会在重复获取连接了.此时,数据源切换失效.请看下面示例代码:
//service 层某一个方法
public Result<Boolean> updateCase() {
//add @1=ds001
//掉哟个数据源1
CaseBaseExample baseExample = new CaseBaseExample();
baseExample.createCriteria().andIdEqualTo(1);
caseBaseMapper.selectByExample(baseExample);
//add doc @2=ds002
//调用数据源2
DocNoticeExample noticeExample = new DocNoticeExample();
noticeExample.createCriteria().andNumNoticeIdEqualTo(2);
docNoticeMapper.selectByExample(noticeExample);
return Result.buildResult(ResponseCode.SUCCESS, "ok", "ok");
}
1 事务传播性=required 时 service获取事务初始化流程
根据上文配置,service层的update方法,事务传播性为 required
在进入这个方法之前,无论如何必须去拿一个事务,要么新建一个,要么用现有的.
请看如下调用逻辑:
每一个被事务拦截的方法,都要走这个流程,来获取事务. 关键点就在这里:
AbstractPlatformTransactionManager.getTransaction(TransactionDefinition definition)
该方法的核心代码如下:
// No existing transaction found -> check propagation behavior to find out how to proceed.
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
throw new IllegalTransactionStateException(
"No existing transaction found for transaction marked with propagation 'mandatory'");
}
else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
SuspendedResourcesHolder suspendedResources = suspend(null);
if (debugEnabled) {
logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition);
}
try {
//如果service 的事务传递性为required,则执行doBegin方法
boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
DefaultTransactionStatus status = newTransactionStatus(
definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
doBegin(transaction, definition);
prepareSynchronization(status, definition);
return status;
}
catch (RuntimeException ex) {
resume(null, suspendedResources);
throw ex;
}
catch (Error err) {
resume(null, suspendedResources);
throw err;
}
}
else {
///如果事务传递性是 suport 则执行这一个分支
// Create "empty" transaction: no actual transaction, but potentially synchronization.
boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null);
}
required 类型的传播事务,代码流转如下
从上面的时序图能够发现,如果事务传播特性是required,则会执行doBegin去获取事务,也就是获取数据源
获取数据源的代码就在上图的最后一个环节,
determineTargetDataSource
中,详细代码如下
/**
* 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");
//这个地方会调用自己实现类DynamicDataSource中的determineCurrentLookupKey方法,就是获取当前线程的数据源.即ds001或者ds002
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
//如果没有拿到当前的looupKey, 则根据以上xml配置里面的defaultTargetDataSource,获取默认连接,就是说,如果当前有设置的key就根据key获取,如果没有,就从动态数据源配置中获取默认的数据源连接. 本文中就是ds001
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;
}
总结:这种情况下,如果一个事务service中有多个数据源的dao操作,会导致找不到数据源.因为数据源是事务启动的时候已经找好了的.不会在执行dao层的时候,重新寻找了.这个时候就会出现类似
Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table 'ds001.doc_notice' doesn't exist
的报错.
2 事务传播性=supported 时 service事务初始化流程
如果事务传播属性为supported,则进入service之前代码不会主动创建事务,但是会new一个事务上下文并且用来保存一些属性到当前的线程.时序图如下所示:
可以看到在这个流程中,创建的事务上下文并没有去执行 doBegin
来拿数据源链接,但是这种情况会不会表示 数据源的切换就没有问题了呢 .
不见得.
请看下文继续分析.
3 正式请求进入service之后的处理流程
上文分析了 在事务情况下,数据源和事务的关系 .下文我们看下,请求进入service之后,在执行dao层之前 的数据源切换流程.
在分析具体执行代码之前,请先看下面时序图:
A 拦截切换部分
这个过程比较简单, 一个普通的拦截器调用.会在获取数据库连接之前,调用线程中保存的数据源key的上下文.后面拿数据库连接,就根据 DataSourceContextHolder.setDataSourceType(DynamicDataSource.USER);
这句代码对其的设定来获取的
B 获取数据源和执行DB操作部分[insert为例]
这个环节是执行数据源和查询的核心逻辑,通过层层的反射调用最终走到 SimpleExecutor.doUpdate
方法,在这里来获取所需要的一切
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
//在这一句 要去获取connection了.....
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.update(stmt);
} finally {
closeStatement(stmt);
}
}
//正式获取 数据库连接
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
//这里回去调用获取连接方法
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
//BaseExecutor方法
protected Connection getConnection(Log statementLog) throws SQLException {
//不管走不走事务,都会从这里去获取数据源链接,此时会从这里请求SpringManagedTransaction的getConnection函数
Connection connection = transaction.getConnection();
if (statementLog.isDebugEnabled()) {
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
return connection;
}
}
//SpringManagedTransaction
@(2_spring技术)Override
public Connection getConnection() throws SQLException {
if (this.connection == null) {
openConnection();
}
return this.connection;
}
///SpringManagedTransaction
private void openConnection() throws SQLException {
//最终会从 DataSourceUtils那里拿到数据源连接
this.connection = DataSourceUtils.getConnection(this.dataSource);
this.autoCommit = this.connection.getAutoCommit();
this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(
"JDBC Connection ["
+ this.connection
+ "] will"
+ (this.isConnectionTransactional ? " " : " not ")
+ "be managed by Spring");
}
}
经过这么多道弯,最终去DataSourceUtils 那里获取数据库连接.在DataSourceUtils中进行了 事务和非事务的判断,并提供了不同情况下 ,怎么那数据库连接的方式.请看如下代码:
/**
* Actually obtain a JDBC Connection from the given DataSource.
* Same as {@link #getConnection}, but throwing the original SQLException.
* <p>Is aware of a corresponding Connection bound to the current thread, for example
* when using {@link DataSourceTransactionManager}. Will bind a Connection to the thread
* if transaction synchronization is active (e.g. if in a JTA transaction).
* <p>Directly accessed by {@link TransactionAwareDataSourceProxy}.
* @param dataSource the DataSource to obtain Connections from
* @return a JDBC Connection from the given DataSource
* @throws SQLException if thrown by JDBC methods
* @see #doReleaseConnection
*/
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
Assert.notNull(dataSource, "No DataSource specified");
//从事务上下文获取ConnectionHolder ,如果要执行的dao层方法没有事务,则这里获取的conHolder是空的
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
conHolder.requested();
//如果service中第一次执行db操作此时还没有连接 ,此时hasConnection 是空的.会从datasource 中获取.
if (!conHolder.hasConnection()) {
logger.debug("Fetching resumed JDBC Connection from DataSource");
conHolder.setConnection(dataSource.getConnection());
}
//如果当前线程已经有了连接,直接返回. 这里就可以看出,如果存在事务,第二次拿取连接不是根据切换之后的key拿的,而是从事务上下文直接返回的.
return conHolder.getConnection();
}
// Else we either got no holder or an empty thread-bound holder here.
logger.debug("Fetching JDBC Connection from DataSource");
//如果没有事务 ,每次都是直接去根据当前线程上下文的key获取动态数据源的指定连接.
Connection con = dataSource.getConnection();
///如果当前有事务上下文,则把之前拿到的连接绑定到当前的事务中去.
if (TransactionSynchronizationManager.isSynchronizationActive()) {
logger.debug("Registering transaction synchronization for JDBC Connection");
// Use same Connection for further JDBC actions within the transaction.
// Thread-bound object will get removed by synchronization at transaction completion.
ConnectionHolder holderToUse = conHolder;
if (holderToUse == null) {
holderToUse = new ConnectionHolder(con);
}
else {
holderToUse.setConnection(con);
}
holderToUse.requested();
TransactionSynchronizationManager.registerSynchronization(
new ConnectionSynchronization(holderToUse, dataSource));
holderToUse.setSynchronizedWithTransaction(true);
if (holderToUse != conHolder) {
TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
}
}
return con;
}
经过以上的分析,可以看出 ,动态数据源只有在没有事务的情况下 ,能正常切换. 不支持分布式事务.如果再一个有多个数据库操作的service中还加了事务,则很有可能会报错.
总结
使用动态数据源要注意什么情况才能安全的使用它呢?
1. 数据源用完之后要清理掉.所以对一个dao操作,要加两个拦截执行之前 设置数据源的key,执行sql之后,清空sql的key. 为什么这么做?
如果用了之后不清理,比如上次用过之后,没有置空数据源的key,然后执行到下一个service时候,当前的key是ds002, 如果要执行的这个service是包含事务的,并且是操作ds001数据库的,这个时候事务的数据源是ds002,将无法操作事务中的dao函数.
数据源拦截代码改造如下
//拦截以后设置数据源
@Before(value = "dbChangeBstKaPointCut()")
public void changeBstKaDB(JoinPoint joinPoint) {
logger.debug("----------------调用bstka数据库连接----------------");
DataSourceContextHolder.setDataSourceType(DynamicDataSource.USER);
}
//用过之后清理
@AfterReturning(value = "dbChangeBstKaPointCut()")
public void resetKaHolder(JoinPoint joinPoint) {
logger.debug("----------------清除bstka数据库连接----------------");
DataSourceContextHolder.clearDataSourceType();
}
2. 有多数据源的service不要使用事务,即便是 supported类型的传播性也会导致出错. 推荐不要用 事务拦截的tx:advice
节点里面 不要配置 <tx:method name="*" propagation="SUPPORTS"/>
这样的话 ,所有的方法都会创建事务上下文,会让我们配置的数据源动态切换失效.