spring动态数据源和事务配合的调研
来源:互联网 发布:爱腾网络延长器接法图 编辑:程序博客网 时间:2024/05/20 16:45
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(); }}//数据源的拦截和切换ComponentAspectpublic 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; }///SpringManagedTransactionprivate 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"/>
这样的话 ,所有的方法都会创建事务上下文,会让我们配置的数据源动态切换失效.
- spring动态数据源和事务配合的调研
- spring无事务的数据源切换,和带事务的数据源切换
- Spring MVC+分布式事务+动态数据源配置
- Spring分布式动态数据源+事务
- spring和mybatis集成(二) 设置spring的动态数据源
- SPRING事务逻辑探究和多数据源解决方案调研
- Spring分布式事务在service中动态切换数据源
- Spring分布式事务在service中动态切换数据源
- Spring分布式事务在service中动态切换数据源
- Spring的自动扫描、数据源配置、AOP和事务等配置
- Transaction事务注解和DynamicDataSource动态数据源切换问题解决
- Spring mvc 动态数据源的配置
- mybatis+spring动态数据源的配置
- spring下动态数据源的切换
- 事务管理器和数据源
- Spring动态切换数据源
- spring动态数据源1
- SPRING动态数据源使用方法
- 如何自定义简单的背景图
- mysql5.7数据库安装完成后如何配置环境变量
- SVM学习笔记
- 帕塞瓦尔定理(能量守恒定理)证明
- overpass language 笔记
- spring动态数据源和事务配合的调研
- 20170516@Map集合遍历方式
- swift 与JS交互( JS调用swift方法)
- 遍历链表 构造并升序输出(解题报告)
- webpack集成bootstrap进行多页面开发
- 《Objective-C基础教程(第2版)》pdf
- 我的第一个Unity3d项目-超级玛丽
- 请写出下列表达式的值,并编写程序验证。
- ##顺序表 编码##